Big refactoring, still really a WIP, but I need to get this committed

This commit is contained in:
Patrick Marsee 2022-12-26 14:08:57 -05:00
parent 9251886638
commit f3db901918
21 changed files with 91 additions and 19 deletions

View file

@ -0,0 +1,10 @@
Metadata-Version: 2.1
Name: gameshell
Version: 0.0.2
Summary: A game engine for text adventures.
Author-email: Patrick Marsee <me@cheesewatergames.net>
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: GNU General Public License v3.0
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown

View file

@ -0,0 +1,21 @@
pyproject.toml
src/gameshell/__init__.py
src/gameshell/__main__.py
src/gameshell/gamebase.py
src/gameshell/gameevents.py
src/gameshell/gameexpr.py
src/gameshell/gamegui.py
src/gameshell/gamelocus.py
src/gameshell/gamemap.py
src/gameshell/gamesequence.py
src/gameshell/gamethings.py
src/gameshell/gameutil.py
src/gameshell/locusdemo.py
src/gameshell/main.py
src/gameshell/shell.py
src/gameshell/tile.py
src/gameshell.egg-info/PKG-INFO
src/gameshell.egg-info/SOURCES.txt
src/gameshell.egg-info/dependency_links.txt
src/gameshell.egg-info/requires.txt
src/gameshell.egg-info/top_level.txt

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@
ruamel.yaml

View file

@ -0,0 +1 @@
gameshell

View file

@ -0,0 +1,6 @@
if __name__ == "__main__":
from . import main
from . import gamebase
sh = main.GameShell(gamebase.GameBase())
sh.menuMode()
sh.run()

View file

@ -0,0 +1,6 @@
if __name__ == "__main__":
from . import main
from . import gamebase
sh = main.GameShell(gamebase.GameBase())
sh.menuMode()
sh.run()

1276
src/gameshell/gamebase.py Normal file

File diff suppressed because it is too large Load diff

125
src/gameshell/gameevents.py Normal file
View file

@ -0,0 +1,125 @@
#gameevents.py
# Experimental: events are ordered in the order that they were created,
# to resolve issues where two events happen at the same time.
_eventNum = 0
def resetEventNum():
global _eventNum
_eventNum = 0
class GameEvent(object):
def __init__(self, eventType: str):
global _eventNum
self.eventType = eventType
# Experimental, also NOT THREAD SAFE!
self.eventNum = _eventNum
_eventNum += 1
def __str__(self):
return "{0} ({1})".format(self.eventType, self.eventNum)
def __eq__(self, other):
if isinstance(other, GameEvent):
return self.eventNum == other.eventNum
else:
return self.eventNum == other
def __ne__(self, other):
if isinstance(other, GameEvent):
return self.eventNum != other.eventNum
else:
return self.eventNum != other
def __lt__(self, other):
if isinstance(other, GameEvent):
return self.eventNum < other.eventNum
else:
return self.eventNum < other
def __le__(self, other):
if isinstance(other, GameEvent):
return self.eventNum <= other.eventNum
else:
return self.eventNum <= other
def __gt__(self, other):
if isinstance(other, GameEvent):
return self.eventNum > other.eventNum
else:
return self.eventNum > other
def __ge__(self, other):
if isinstance(other, GameEvent):
return self.eventNum >= other.eventNum
else:
return self.eventNum >= other
class NoOpEvent(GameEvent):
def __init__(self):
super(NoOpEvent, self).__init__('noop')
class GoEvent(GameEvent):
def __init__(self, actor, path, speed, action = None, locus = None, closeEnough = True, timeTaken = 0):
super(GoEvent, self).__init__('go')
self.actor = actor
self.path = path
self.speed = speed
self.action = action
self.locus = locus
self.closeEnough = closeEnough
self.timeTaken = timeTaken
@property
def pos(self):
return self.path[0]
class ArriveEvent(GameEvent):
def __init__(self, actor, pos, speed, action = None, locus = None, timeTaken = 0):
super(ArriveEvent, self).__init__('arrive')
self.actor = actor
self.pos = pos
self.speed = speed
self.action = action
self.locus = locus
self.timeTaken = timeTaken
class UseEvent(GameEvent):
def __init__(self, actor, thing, args):
super(UseEvent, self).__init__('use')
self.actor = actor
self.thing = thing
self.args = args
class UseOnEvent(GameEvent):
def __init__(self, actor, item, target, args):
super(UseOnEvent, self).__init__('useon')
self.actor = actor
self.target = target # thing can be a coordinate pair?
self.item = item
self.args = args
class TakeEvent(GameEvent):
def __init__(self, actor, item):
super(TakeEvent, self).__init__('take')
self.actor = actor
self.item = item
class DropEvent(GameEvent):
def __init__(self, actor, item):
super(DropEvent, self).__init__('drop')
self.actor = actor
self.item = item
class BehaveEvent(GameEvent):
def __init__(self, actor):
super(BehaveEvent, self).__init__('behave')
self.actor = actor
# |\_/|
# /0 0\
# \o/

455
src/gameshell/gamegui.py Normal file
View file

@ -0,0 +1,455 @@
# gamegui.py
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
from tile import Tile
from gamebase import GameBase
import io
class App(ttk.Frame):
def __init__(self, master, gameBase):
#print('Initializing...')
super(App, self).__init__(master)
self.gameBase = gameBase
self.gameBase.outstream = io.StringIO()
self.gameBase.requestInput = self.requestInput
self.gameBase.onLevelLoad = self.getGraphics
self.selected = (-1, -1)
self.parent = master
self.mapSize = (800, 450)
self.hasChanged = False
self.fileName = 'Untitled'
self.invTuple = tuple(self.gameBase.playerInv.keys())
self.invNames = StringVar(value=self.invTuple)
#self.project = Project()
#self.tool = StringVar()
#self.brush = StringVar()
#self.history = []
#self.redos = []
self.useOnCont = None # useOn continued
#self.selectedArea = (0, 0, self.project.x-1, self.project.y-1)
self.parent.clipboard_clear()
self.parent.clipboard_append('')
self.grid()
self.createWidgets()
self.makeTheDisplayWork()
self.bindKeys()
#print('Done.')
def createWidgets(self):
#print('Creating Widgets...')
self.initWidth, self.initHeight = 0, 0
# create menu bar
self.createMenuBar()
# create map
self.createMapView()
# create text out
self.createTextOut()
# create inventory list
self.createInventoryView()
#self.tool.set('setone')
#brushes = ttk.Combobox(self, textvariable = self.brush, values = tuple(sorted(Project.tileID.keys())))
#brushes.grid(column = 0, row = 6, sticky = (E, W))
#brushes.state(['readonly'])
#brushes.set('Dirt')
#ttk.Separator(self, orient = VERTICAL).grid(column = 1, row = 0, rowspan = 9, sticky = (N, S))
ttk.Sizegrip(self).grid(column = 1, row = 1, sticky = (S, E))
self.columnconfigure(0, weight = 1)
self.rowconfigure(0, weight = 1)
#print('Done.')
def createMenuBar(self):
#print('Creating the menubar...')
menubar = Menu(self.parent)
menuFile = Menu(menubar)
#menuEdit = Menu(menubar)
menuHelp = Menu(menubar)
menubar.add_cascade(menu = menuFile, label = 'File')
menuFile.add_command(label = 'New', command = lambda: self.loadLevel('maps/apartment.xml'), accelerator = 'Ctrl+N')
menuFile.add_command(label = 'Open...', command = self.fileOpen, accelerator = 'Ctrl+O')
menuFile.add_command(label = 'Save', command = self.fileSave, accelerator = 'Ctrl+S')
menuFile.add_command(label = 'Save As...', command = self.fileSaveAs)
menuFile.add_separator()
menuFile.add_command(label = 'Exit', command = self.onQuit, accelerator = 'Ctrl+Q')
#menubar.add_cascade(menu = menuEdit, label = 'Edit')
#menuEdit.add_command(label = 'Undo', command = self.undo, accelerator = 'Ctrl+Z')
#menuEdit.add_command(label = 'Redo', command = self.redo, accelerator = 'Ctrl+Y')
#menuEdit.add_separator()
#menuEdit.add_command(label = 'Cut', command = self.cut, accelerator = 'Ctrl+X')
#menuEdit.add_command(label = 'Copy', command = self.copy, accelerator = 'Ctrl+C')
#menuEdit.add_command(label = 'Paste', command = self.paste, accelerator = 'Ctrl+V')
menubar.add_cascade(menu = menuHelp, label = 'Help')
menuHelp.add_command(label = 'About', command = self.about)
menuHelp.add_separator()
menuHelp.add_command(label = 'Read Me', command = self.readme)
self.parent.configure(menu = menubar)
#print('Done.')
def createMapView(self):
mapFrame = ttk.LabelFrame(self, text = "map")
mapFrame.grid(column = 0, row = 0)
self.levelDisplay = Canvas(mapFrame, width = self.mapSize[0], height = self.mapSize[1], scrollregion = '0 0 2048 2048')
self.levelDisplay.grid(column = 0, row = 0)
vbar = ttk.Scrollbar(mapFrame, orient = VERTICAL, command = self.levelDisplay.yview)
self.levelDisplay.configure(yscrollcommand = vbar.set)
vbar.grid(column = 1, row = 0, sticky = (N, S))
hbar = ttk.Scrollbar(mapFrame, orient = HORIZONTAL, command = self.levelDisplay.xview)
self.levelDisplay.configure(xscrollcommand = hbar.set)
hbar.grid(column = 0, row = 1, sticky = (E, W))
self.bind('<Configure>', self.onResize)
def createTextOut(self):
textFrame = ttk.LabelFrame(self, text = "Info")
textFrame.grid(column = 0, row = 1)
self.infoDisplay = Text(textFrame, width = 80, height = 8, state = 'disabled')
self.infoDisplay.grid(column = 0, row = 0)
vbar = ttk.Scrollbar(textFrame, orient = VERTICAL, command = self.infoDisplay.yview)
self.infoDisplay.configure(yscrollcommand = vbar.set)
vbar.grid(column = 1, row = 0, sticky = (N, S))
self.infoCursor = 0
def createInventoryView(self):
invFrame = ttk.LabelFrame(self, text = "Inventory")
invFrame.grid(column = 1, row = 0)
self.invDisplay = Listbox(invFrame, height = 24, listvariable = self.invNames)
self.invDisplay.grid(column = 0, row = 0)
vbar = ttk.Scrollbar(invFrame, orient = VERTICAL, command = self.invDisplay.yview)
self.invDisplay.configure(yscrollcommand = vbar.set)
vbar.grid(column = 1, row = 0, sticky = (N, S))
def getGraphics(self):
self.tiles = {}
for i in range(len(self.gameBase.level.floorColors)):
self.tiles['e{0}'.format(i)] = Tile(self.gameBase.level.floorColors[i])
for i in range(len(self.gameBase.level.wallColors)):
self.tiles['w{0}'.format(i)] = Tile(self.gameBase.level.wallColors[i])
self.tiles['player'] = Tile('clear', 'blue', 'o')
for thing in self.gameBase.level.thingNames:
graphic = self.gameBase.level.thingNames[thing].graphic
self.tiles[thing] = Tile(graphic[0], graphic[1], graphic[2])
def makeTheDisplayWork(self):
#print('Making the display work...')
# level display
self.context = Menu(self.levelDisplay)
self.context.add_command(label = 'Load Map', command = lambda: self.loadLevel('maps/apartment.yml'))
self.context.add_command(label = 'Go', command = lambda: self.go(self.selected[0], self.selected[1]))
self.context.add_command(label = 'Run', command = lambda: self.go(self.selected[0], self.selected[1], True))
self.context.add_command(label = 'Look', command = lambda: self.look(self.selected[0], self.selected[1]))
self.context.add_command(label = 'Use', command = lambda: self.use(self.selected[0], self.selected[1]))
self.context.add_command(label = 'Take', command = lambda: self.take(self.selected[0], self.selected[1]))
#self.context.add_command(label = 'Cut', command = self.cut, accelerator = 'Ctrl+X')
#self.context.add_command(label = 'Goblin...')
#self.context.entryconfigure('Goblin...', command = self.configureGoblin, state = DISABLED)
#self.refreshDisplay()
self.levelDisplay.bind('<1>', self.handleClick)
self.levelDisplay.bind('<Double-1>', self.handleDoubleClick)
#self.levelDisplay.bind('<B1-Motion>', self.handleDragEvent)
#self.levelDisplay.bind('<ButtonRelease-1>', self.handleReleaseEvent)
self.levelDisplay.bind('<3>', self.handleRightClick)
# inventory list
self.invContext = Menu(self.invDisplay)
self.invContext.add_command(label = 'Look', command = lambda: self.look(self.invTuple[self.invDisplay.curselection()[0]]))
self.invContext.add_command(label = 'Use', command = lambda: self.use(self.invTuple[self.invDisplay.curselection()[0]]))
self.invContext.add_command(label = 'Use on...', command = lambda: self.useOn(self.invTuple[self.invDisplay.curselection()[0]]))
self.invContext.add_command(label = 'Drop', command = lambda: self.drop(self.invTuple[self.invDisplay.curselection()[0]]))
self.invDisplay.bind('<<ListboxSelect>>', lambda e: self.look(self.invTuple[self.invDisplay.curselection()[0]]))
self.invDisplay.bind('<3>', lambda e: self.invContext.post(e.x_root, e.y_root))
#print('Done.')
pass
def bindKeys(self):
self.parent.bind('<Control-n>', lambda e: self.fileNew())
self.parent.bind('<Control-o>', lambda e: self.fileOpen())
self.parent.bind('<Control-s>', lambda e: self.fileSave())
#self.parent.bind('<Control-z>', lambda e: self.undo())
#self.parent.bind('<Control-y>', lambda e: self.redo())
#self.parent.bind('<Control-x>', lambda e: self.cut())
#self.parent.bind('<Control-c>', lambda e: self.copy())
#self.parent.bind('<Control-v>', lambda e: self.paste())
self.parent.bind('<Control-q>', lambda e: self.onQuit())
def refreshDisplay(self):
#print('Refreshing the display...')
# refresh the map
self.levelDisplay.delete('all')
for y in range(self.gameBase.level.dimensions[1]):
for x in range(self.gameBase.level.dimensions[0]):
pos = self.gameBase.level.mapMatrix[y][x]
if pos[0] == 'w':
self.tiles['w{0}'.format(pos[1])].paint(self.levelDisplay, x, y)
else:
self.tiles['e{0}'.format(pos[1])].paint(self.levelDisplay, x, y)
for name in self.gameBase.level.thingNames:
thing = self.gameBase.level.getThingByName(name)
self.tiles[name].paint(self.levelDisplay, thing.x, thing.y)
self.tiles['player'].paint(self.levelDisplay, self.gameBase.playerx, self.gameBase.playery)
# refresh the info box
self.gameBase.outstream.seek(self.infoCursor)
#print(self.gameBase.outstream.tell())
#print(self.gameBase.outstream.read())
self.infoDisplay['state'] = 'normal'
self.infoDisplay.insert('end', self.gameBase.outstream.read())
self.infoDisplay.see('end -1 chars')
self.infoDisplay['state'] = 'disabled'
self.infoCursor = self.gameBase.outstream.tell()
#print(self.infoCursor)
#print('Done.')
def handleClick(self, event):
x = int(self.levelDisplay.canvasx(event.x) / 32)
y = int(self.levelDisplay.canvasy(event.y) / 32)
if self.useOnCont == None:
if (x, y) != self.selected:
self.selected = (x, y)
self.look(x, y)
else:
self.useOn(x, y)
def handleDoubleClick(self, event):
x = int(self.levelDisplay.canvasx(event.x) / 32)
y = int(self.levelDisplay.canvasy(event.y) / 32)
thing = self.gameBase.level.getThingAtCoords(x, y)
if thing != None and thing.useable:
self.use(x, y)
else:
self.go(x, y)
def handleRightClick(self, event):
x = int(self.levelDisplay.canvasx(event.x) / 32)
y = int(self.levelDisplay.canvasy(event.y) / 32)
self.selected = (x, y)
self.context.post(event.x_root, event.y_root)
#def openInvContext(self, event):
# self.invContext.post(event.x_root, event.y_root)
def handleDragEvent(self, event):
x = min(int(self.levelDisplay.canvasx(event.x) / 32), self.project.x-1)
y = min(int(self.levelDisplay.canvasy(event.y) / 32), self.project.y-1)
if self.tool.get() == 'select':
rect = self.levelDisplay.find_withtag('selectArea')
self.selectedArea = (min(x, self.firstX), min(y, self.firstY),
max(x, self.firstX), max(y, self.firstY))
self.levelDisplay.coords(rect,
(self.selectedArea[0]*32, self.selectedArea[1]*32,
self.selectedArea[2]*32+31, self.selectedArea[3]*32+31))
def handleReleaseEvent(self, event):
x = int(self.levelDisplay.canvasx(event.x) / 32)
y = int(self.levelDisplay.canvasy(event.y) / 32)
if self.tool.get() == 'select':
rect = self.levelDisplay.find_withtag('selectArea')
self.selectedArea = (min(x, self.firstX), min(y, self.firstY),
max(x, self.firstX), max(y, self.firstY))
self.levelDisplay.coords(rect,
(self.selectedArea[0]*32, self.selectedArea[1]*32,
self.selectedArea[2]*32+31, self.selectedArea[3]*32+31))
tiles = self.project.getTiles(self.selectedArea)
for y in range(len(tiles)):
for x in range(len(tiles[0])):
if type(tiles[y][x]) == Project.GoblinSettings:
self.context.entryconfigure('Goblin...', state = NORMAL)
break
break
else: pass
def loadLevel(self, fileName):
self.gameBase.loadMap([fileName])
#self.getGraphics()
self.refreshDisplay()
def go(self, x, y, running = False):
#print("go called")
#print(x, y)
if running:
self.gameBase.go(['-r', str(x), str(y)])
else:
self.gameBase.go([str(x), str(y)])
self.gameBase.gameEventLoop()
self.refreshDisplay() #inefficient, but will work for now.
def look(self, x, y = 1):
#print("look called")
#if x == self.gameBase.playerx and y == self.gameBase.playery:
# self.gameBase.look([
if isinstance(x, int):
thing = self.gameBase.level.getThingAtCoords(x, y)
if thing != None:
self.gameBase.look([thing.name])
else:
self.gameBase.look([])
else:
self.gameBase.look([x])
self.gameBase.gameEventLoop()
self.refreshDisplay() #inefficient, but will work for now.
def use(self, x, y = 1):
if isinstance(x, int):
self.gameBase.use([str(x), str(y)])
else:
self.gameBase.use([x])
self.gameBase.gameEventLoop()
self.refreshDisplay() #inefficient, but will work for now.
def useOn(self, x, y = 1):
if isinstance(x, int):
self.gameBase.use([self.useOnCont, 'on', str(x), str(y)])
self.useOnCont = None
self.gameBase.gameEventLoop()
self.refreshDisplay()
else: # x is a string
self.useOnCont = x
print("Click on something to use {0} on.".format(x), file = self.gameBase.outstream)
self.refreshDisplay()
def take(self, x, y):
self.gameBase.take([str(x), str(y)])
self.gameBase.gameEventLoop()
self.refreshDisplay() #inefficient, but will work for now.
self.invTuple = tuple(self.gameBase.playerInv.keys())
self.invNames.set(self.invTuple)
def drop(self, item):
self.gameBase.drop([item])
self.gameBase.gameEventLoop()
self.refreshDisplay()
self.invTuple = tuple(self.gameBase.playerInv.keys())
self.invNames.set(self.invTuple)
def requestInput(self, prompt = ''):
answer = messagebox.askyesno(message=prompt, icon='question', title='Prompt')
if answer:
return 'y'
else:
return 'n'
def fileNew(self):
#print('Creating a new project...')
if self.askToSave():
self.newDialog = Toplevel(self.parent)
self.newDialog.title('New Project')
newFrame = Frame(self.newDialog)
newFrame.grid()
self.hei = StringVar()
self.wid = StringVar()
ttk.Label(newFrame, text = 'Width:').grid(column = 0, row = 0, sticky = W)
ttk.Label(newFrame, text = 'Height:').grid(column = 0, row = 1, sticky = W)
Spinbox(newFrame, from_ = 16, to = 255, increment = 1, textvariable = self.wid
).grid(column = 1, row = 0, sticky = W)
Spinbox(newFrame, from_ = 16, to = 255, increment = 1, textvariable = self.hei
).grid(column = 1, row = 1, sticky = W)
ttk.Button(newFrame, text = 'Create', command = self.__confirmNewDimensions,
default = 'active').grid(column = 0, row = 2)
ttk.Button(newFrame, text = 'Cancel', command = self.newDialog.destroy
).grid(column = 1, row = 2)
#print('Done.')
def fileOpen(self):
#print('Opening a project...')
if self.askToSave():
newName = filedialog.askopenfilename(defaultextension = '.dat', initialdir = 'saves')
if newName != '':
self.fileName = newName
self.gameBase.loadGame((self.fileName,))
self.gameBase.gameEventLoop()
self.refreshDisplay()
self.invTuple = tuple(self.gameBase.playerInv.keys())
self.invNames.set(self.invTuple)
#print('Done.')
def fileSave(self):
#print('Saving a project...')
newName = self.fileName
if self.fileName == 'Untitled':
self.fileSaveAs()
elif newName != '':
self.fileName = newName
self.gameBase.saveGame((self.fileName,))
self.hasChanged = False
#print('Done.')
def fileSaveAs(self):
newName = filedialog.asksaveasfilename(defaultextension = '.dat', initialdir = 'saves')
if newName != '':
self.fileName = newName
self.gameBase.saveGame((self.fileName,))
self.hasChanged = False
def onQuit(self):
if self.askToSave():
exit()
def askToSave(self):
if self.hasChanged:
insecure = messagebox.askyesnocancel(
message = 'Do you want to save ' + self.fileName + ' before continuing?',
icon = 'warning', title = 'New File')
print(type(insecure))
if insecure == None: return False
elif insecure == True:
self.fileSave()
return True
else: return True
else: return True
def about(self):
self.newDialog = Toplevel(self.parent)
self.newDialog.title('About')
newFrame = Frame(self.newDialog)
newFrame.grid()
ttk.Label(newFrame, text = 'I Am Gnome Level Editor v.0.9.0013').grid()
ttk.Button(newFrame, text = 'Okay', command = self.newDialog.destroy).grid()
def readme(self):
self.newDialog = Toplevel(self.parent)
self.newDialog.title('About')
newFrame = Frame(self.newDialog)
newFrame.grid()
text = Text(newFrame, width=80, height=40, wrap = 'word')
text.grid(column = 0, row = 0)
sbar = ttk.Scrollbar(newFrame, orient = VERTICAL, command = text.yview)
sbar.grid(column = 1, row = 0, sticky = (N, S, W))
text.configure(yscrollcommand = sbar.set)
text.state(['disabled'])
file = open('iag_readme.txt', 'r')
text.insert('1.0', file.read())
def onResize(self, event):
if self.initWidth == 0 and self.initHeight == 0:
self.initWidth = event.width
self.initHeight = event.height
else:
wDelta = event.width - int(self.initWidth)
hDelta = event.height - int(self.initHeight)
self.levelDisplay.configure(width = self.mapSize[0] + wDelta, height = self.mapSize[1] + hDelta)
# main
if __name__ == '__main__':
root = Tk()
root.title('Game Gui (debug)')
#root.geometry("1024x768")
#root.resizable(FALSE, FALSE)
root.option_add('*tearOff', FALSE)
root.columnconfigure(0, weight = 1)
root.rowconfigure(0, weight = 1)
newApp = App(root, GameBase())
newApp.grid(sticky = (N, S, E, W))
root.mainloop()

336
src/gameshell/gamelocus.py Normal file
View file

@ -0,0 +1,336 @@
# gamelocus.py
import math as _mt
class Locus(object):
"""Abstract base class for locus objects."""
def __init__(self, **kwargs):
"""Base locus constructor."""
pass
def __contains__(self, item):
"""Is this point within the locus?"""
return False
def __iter__(self):
return iter(())
def __repr__(self):
return "Locus()"
class PointLocus(Locus):
"""A locus that defines a single tile."""
def __init__(self, x, y, **kwargs):
"""Take the coordinates for a point."""
self.x = x
self.y = y
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
return item[0] == self.x and item[1] == self.y
def __iter__(self):
return iter(((self.x, self.y),)) # just an iterator of the one tile
def __repr__(self):
return "PointLocus({}, {})".format(self.x, self.y)
class LineLocus(Locus):
"""A locus that defines all square tiles that intersect a given line segment."""
def __init__(self, x1, y1, x2, y2, thick = False, **kwargs):
"""Take the coordinates for the two endpoints."""
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.thick = thick
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
# Is it outside the bounding box for this line?
if x > max(self.x1, self.x2) or x < min(self.x1, self.x2) or y > max(self.y1, self.y2) or y < min(self.y1, self.y2):
return False
# Is this line straight up and down, or left and right?
elif self.x1 == self.x2 or self.y1 == self.y2:
# In this case, the line takes up the whole bounding box, so it must
# be true since it passed the first step to get here.
return True
else:
slope = (self.y2 - self.y1) / (self.x2 - self.x1)
intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope
f = lambda z: z * slope + intercept
if self.thick:
if slope > 0:
return _mt.ceil(f(x)-1) <= y and _mt.floor(f(x+1)+1) > y
else:
return _mt.floor(f(x)+1) > y and _mt.ceil(f(x+1)-1) <= y
else:
if slope > 0:
return _mt.floor(f(x)) <= y and _mt.ceil(f(x+1)) > y
else:
return _mt.ceil(f(x)) > y and _mt.floor(f(x+1)) <= y
def __iter__(self):
# avoid infinite slope case
if self.x1 == self.x2:
miny, maxy = min(self.y1, self.y2), max(self.y1, self.y2) + 1
ret = [(self.x1, y) for y in range(miny, maxy)]
return iter(ret)
# for convenience: it's easy to calculate if it's horizontal too.
elif self.y1 == self.y2:
minx, maxx = min(self.x1, self.x2), max(self.x1, self.x2) + 1
ret = [(x, self.y1) for x in range(minx, maxx)]
return iter(ret)
else:
slope = (self.y2 - self.y1) / (self.x2 - self.x1)
intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope
f = lambda x: x * slope + intercept
lx, ly = min((self.x1, self.y1), (self.x2, self.y2))
rx, ry = max((self.x1, self.y1), (self.x2, self.y2))
ret = []
# edge case 1: the first half column
if slope > 0:
maxy = _mt.ceil(f(lx+1))
if self.thick:
maxy = _mt.floor(f(lx+1)+1)
for y in range(ly, maxy):
ret.append((lx, y))
else:
maxy = _mt.floor(f(lx+1))-1
if self.thick:
maxy = _mt.ceil(f(lx+1)-2)
for y in range(ly, maxy, -1):
ret.append((lx, y))
# Usual case: the line between the end points
for x in range(lx+1, rx):
if slope > 0:
miny = _mt.floor(f(x))
maxy = _mt.ceil(f(x+1))
if self.thick:
miny = _mt.ceil(f(x)-1)
maxy = _mt.floor(f(x+1)+1)
for y in range(miny, maxy):
ret.append((x, y))
else:
miny = _mt.ceil(f(x)-1)
maxy = _mt.floor(f(x+1))-1
if self.thick:
miny = _mt.floor(f(x))
maxy = _mt.ceil(f(x+1)-2)
for y in range(miny, maxy, -1):
ret.append((x, y))
# edge case 2: the last half column
if slope > 0:
miny = _mt.floor(f(rx))
if self.thick:
miny = _mt.ceil(f(rx)-1)
for y in range(miny, ry+1):
ret.append((rx, y))
else:
miny = _mt.ceil(f(rx)-1)
if self.thick:
miny = _mt.floor(f(rx))
for y in range(miny, ry-1, -1):
ret.append((rx, y))
return iter(ret)
def __repr__(self):
return "LineLocus({}, {}, {}, {}, thick={})".format(self.x1, self.y1, self.x2, self.y2, self.thick)
class RectLocus(Locus):
"""A locus that defines the outline of a rectangle."""
def __init__(self, x1, y1, x2, y2, **kwargs):
self.lx = min(x1, x2)
self.rx = max(x1, x2)
self.ly = min(y1, y2)
self.ry = max(y1, y2)
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
if x < self.lx or x > self.rx or y < self.ly or y > self.ry:
return False
elif x == self.lx or x == self.rx or y == self.ry or y == self.ly:
return True
else:
return False
def __iter__(self):
ret = [(x, self.ly) for x in range(self.lx, self.rx)]
ret.extend([(self.rx, y) for y in range(self.ly, self.ry)])
ret.extend([(x, self.ry) for x in range(self.rx, self.lx, -1)])
ret.extend([(self.lx, y) for y in range(self.ry, self.ly, -1)])
return iter(ret)
def __repr__(self):
return "RectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry)
class FilledRectLocus(Locus):
"""A locus that defines all points within a rectangle."""
def __init__(self, x1, y1, x2, y2, **kwargs):
self.lx = min(x1, x2)
self.rx = max(x1, x2)
self.ly = min(y1, y2)
self.ry = max(y1, y2)
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
return x >= self.lx and x <= self.rx and y >= self.ly and y <= self.ry
def __iter__(self):
return iter([(x, y) for y in range(self.ly, self.ry+1) for x in range(self.lx, self.rx+1)])
def __repr__(self):
return "FilledRectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry)
class CircleLocus(Locus):
"""A locus that defines the outline of a circle."""
def __init__(self, x, y, radius, **kwargs):
self.x = x + 0.5
self.y = y + 0.5
self.r = radius
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
if self.x == x + 0.5 and (y == _mt.floor(self.y + self.r) or y == _mt.floor(self.y - self.r)):
return True
elif self.y == y + 0.5 and (x == _mt.floor(self.x + self.r) or x == _mt.floor(self.x - self.r)):
return True
else:
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
# In this case, the circle is small enough that the previous
# checks would have cought every possible tile other than the center.
return x == _mt.floor(self.x) and y == _mt.floor(self.y)
if y + 0.5 > self.y:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) > self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) > self.r
else:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) > self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y - self.y)**2) > self.r
def __iter__(self):
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
if self.r > 0.5:
ret = ((_mt.floor(self.x), _mt.floor(self.y)),
(_mt.floor(self.x) + 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) + 1),
(_mt.floor(self.x) - 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) - 1))
return iter(ret)
else:
return iter(((_mt.floor(self.x), _mt.floor(self.y)),))
else:
f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y
# The topmost square: the 'keystone' if you will.
ret = [(_mt.floor(self.x), _mt.floor(self.y + self.r))]
# All squares between the keystone and the rightmost column.
for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)):
ly = _mt.floor(f(x))
ry = _mt.floor(f(x+1)) - 1
for y in range(ly, ry, -1):
ret.append((x, y))
# The last column, minus the bottom square
x = _mt.floor(self.x + self.r)
for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1):
ret.append((x, y))
# Finish the circle by copying the coordinates we already have.
ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret]
return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret])
def __repr__(self):
return "CircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r)
class FilledCircleLocus(Locus):
"""A locus that defines all points within a circle."""
def __init__(self, x, y, radius, **kwargs):
self.x = x + 0.5
self.y = y + 0.5
self.r = radius
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
if self.x == x + 0.5 and (y <= _mt.floor(self.y + self.r) and y >= _mt.floor(self.y - self.r)):
return True
elif self.y == y + 0.5 and (x <= _mt.floor(self.x + self.r) and x >= _mt.floor(self.x - self.r)):
return True
else:
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
# In this case, the circle is small enough that the previous
# checks would have cought every possible tile other than the center.
return x == _mt.floor(self.x) and y == _mt.floor(self.y)
if y + 0.5 > self.y:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r
else:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r
def __iter__(self):
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
if self.r > 0.5:
ret = ((_mt.floor(self.x), _mt.floor(self.y)),
(_mt.floor(self.x) + 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) + 1),
(_mt.floor(self.x) - 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) - 1))
return iter(ret)
else:
return iter(((_mt.floor(self.x), _mt.floor(self.y)),))
else:
f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y
# The topmost square: the 'keystone' if you will.
ret = [(_mt.floor(self.x), y) for y in range(_mt.floor(self.y + self.r), _mt.floor(self.y), -1)]
# All squares between the keystone and the rightmost column.
ry = _mt.floor(self.y)
for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)):
ly = _mt.floor(f(x))
for y in range(ly, ry, -1):
ret.append((x, y))
# The last column, minus the bottom square
x = _mt.floor(self.x + self.r)
for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1):
ret.append((x, y))
# Finish the circle by copying the coordinates we already have.
ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret]
return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret] + [(_mt.floor(self.x), _mt.floor(self.y))])
def __repr__(self):
return "FilledCircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r)
class SetLocus(set, Locus):
"""A locus that defines a set of given arbitrary points."""
def __init__(self, *args, **kwargs):
super(SetLocus, self).__init__(*args, **kwargs)
def __repr__(self):
return "SetLocus({})".format(tuple(self))

667
src/gameshell/gamemap.py Normal file
View file

@ -0,0 +1,667 @@
#gamemap.py
import re
import heapq
import ruamel.yaml
import math as _mt
from . import gamethings as _gt
from . import gamelocus as _gl
from ruamel.yaml.comments import CommentedMap
class Singleton(object):
"""This is a super basic class (would be a struct in other languages)
that represents where a singleton Thing should be in a map."""
yaml_flag = u'!Singleton'
def __init__(self, name: str, x: int, y: int):
self.name = name
self.x = x
self.y = y
@classmethod
def to_yaml(cls, representer, node):
representer.represent_mapping({
'name': self.name,
'location': (self.x, self.y)
})
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# since all parts are necessary, I won't bother with if statements
# and let it crash.
if not isinstance(parts['name'], str):
raise RuntimeError("Name must be a string.")
if not isinstance(parts['location'], list):
raise RuntimeError("Location must be a list.")
if not (isinstance(parts['location'][0], int) and isinstance(parts['location'][1], int)):
raise RuntimeError("Coordinates must be integers.")
return cls(parts['name'], parts['location'][0], parts['location'][1])
class MapError(RuntimeError):
pass
class GameMap(object):
# Matrix tile codes:
# e: empty (0)
# w: wall (0)
# regular expressions
tileRegex = re.compile(r'([a-z ])([0-9]+|[ ])')
matrixRegex = re.compile(r'(?:[ \t]*(?:[a-z ](?:[0-9]+|[ ]))+(\n))+')
yaml = ruamel.yaml.YAML()
yaml.register_class(_gt.Prefab)
yaml.register_class(_gt.Item)
yaml.register_class(_gt.Useable)
yaml.register_class(_gt.NPC)
yaml.register_class(_gt.Door)
yaml.register_class(_gt.MapExit)
yaml.register_class(_gt.MapEntrance)
yaml.register_class(Singleton)
def __init__(self, name, graph, matrix, dimensions):
self.name = name
self.openingText = ""
self.mapGraph = graph
self.mapMatrix = matrix
self.dimensions = dimensions
self.things = {} # int thingID : thing
self.thingPos = {} # int location : list of int thingIDs
self.thingNames = {} # str name: list of int thingIDs
self.playerStart = (1, 1)
self.description = "The area is completely blank."
self.floorColors = []
self.wallColors = []
self.persistent = []
self.enterScript = ''
self.version = 'square 1'
@staticmethod
def __cleanStr(text: str, end = '\n'):
if text == None:
return text
text = text.strip()
i = 0
while True:
i = text.find('\n', i)
if i == -1:
break
j = i+1
if len(text) > j:
while text[j] in ' \t':
j += 1 # just find the first non-space chartacter
text = text[:i+1] + text[j:]
i += 1
else:
break
return text.replace('\n', end)
@staticmethod
def resolvePrefab(prefabs: dict, thing: _gt.Thing):
"""Resolve what type of prefab a thing is. If it isn't a prefab or there are no prefabs loaded, just return the thing."""
if prefabs == None or len(prefabs) == 0 or not isinstance(thing, _gt.Prefab):
return thing
if thing.name not in prefabs:
raise MapError(f"Attempted to load an unrecognized prefab named {thing.name}.")
return prefabs[thing.name].fromPrefab(thing)
@staticmethod
def read(infile = None, prevMap = None, singletons = None, prefabs = None, preLoaded = False, nextThing = 0):
"""Read map data and return a Map object. If infile is not provided, then
it will read from stdin. Otherwise, it should be a valid file name.
Entering a map through stdin will be obsolete once testing is over."""
info = None
tryToRead = True
if infile != None:
try:
with open(infile, 'r') as f:
info = GameMap.yaml.load(f)
except OSError as e:
print("The file could not be read.")
return None, nextThing
else:
raise MapError("No file was specified for loading.")
# Future feature: different map standards, with cool special features
# like hex tiles, or walls in-between tiles.
# For now just 'square 1': square tiles, version 1.
version = 'square 1'
if 'version' in info:
if info['version'] in ('square 1'):
version = info['version']
# Now what we do with the data
mat = None
if 'layout' not in info:
raise MapError('No layout in {0}.'.format(infile))
layout = info['layout']
if layout != None:
#print(layout.text)
match = GameMap.matrixRegex.match(layout.lstrip())
if match == None:
raise MapError('Map read a file without a map matrix.')
mat = match.group()
else:
raise MapError('Map read a file without a map matrix.')
# generate matrix and graph first
mapMatrix, mapGraph, dimensions = GameMap.parseMatrix(mat)
level = GameMap(infile, mapGraph, mapMatrix, dimensions)
# Now, load other info
nextThing = GameMap.loadThings(level, info, prevMap, singletons, prefabs, preLoaded, nextThing)
return level, nextThing
@staticmethod
def parseMatrix(matrixStr):
"""Returns a map graph as an adjacency list, as well as the matrix as a
list of lists of tuples."""
# Make the matrix first
mat = [[]]
x = 0
y = 0
l = 0
while len(matrixStr) > 0:
tile = GameMap.tileRegex.match(matrixStr)
if tile != None:
tileType = tile.group(1)
tileNum = tile.group(2)
if tileType == ' ':
tileType = 'e'
if tileNum == ' ':
tileNum = '0'
mat[l].append((tileType, int(tileNum)))
#x += 1
matrixStr = matrixStr[len(tile.group()):]
elif matrixStr[0] == '\n':
if x == 0:
x = len(mat[l])
elif x != len(mat[l]):
raise MapError("Map matrix has jagged edges.")
l += 1
#x = 0
mat.append([])
i = 1
while i < len(matrixStr) and matrixStr[i] in ' \t\n':
i += 1
if i == len(matrixStr):
matrixStr = ''
else:
matrixStr = matrixStr[i:]
else: # This should happen when it finishes?
raise MapError("Unexpected token in map matrix: '{0}'".format(matrixStr))
y = len(mat) - 1
# Now for the graph
numTiles = x * y
dim = (x, y)
graph = [[] for j in range(numTiles)]
passable = ('e', 'd', 'x', 'p', 'i')
for j in range(y):
for i in range(x):
if mat[j][i][0] in passable:
here = GameMap.__coordsToInt(i, j, x)
if i > 0 and mat[j][i-1][0] in passable:
there = GameMap.__coordsToInt(i-1, j, x)
graph[here].append(there)
graph[there].append(here)
if j > 0 and mat[j-1][i][0] in passable:
there = GameMap.__coordsToInt(i, j-1, x)
graph[here].append(there)
graph[there].append(here)
return mat, graph, dim
@staticmethod
def loadThings(level, info, prevMap = None, singletons = None, prefabs = None, preLoaded = False, nextThing = 0):
"""load the things from the xml part of the map file."""
if 'openingText' in info:
level.openingText = info['openingText']
if 'playerStart' in info:
level.playerStart = info['playerStart']
if 'description' in info:
level.description = info['description']
if 'enterScript' in info:
level.enterScript = info['enterScript']
# get map colors
if 'floorColors' in info:
level.floorColors = info['floorColors']
if 'wallColors' in info:
level.wallColors = info['wallColors']
if len(level.floorColors) == 0:
level.floorColors.append('#9F7F5F')
if len(level.wallColors) == 0:
level.wallColors.append('#7F3F0F')
# get things
hasKnownEntrance = False
if 'loadOnce' in info and not preLoaded:
for thing in info['loadOnce']:
#print(type(thing))
if isinstance(thing, _gt.Thing):
nextThing = level.addThing(GameMap.resolvePrefab(prefabs, thing), prefabs, nextThing, True)
else:
raise MapError("Non-thing loaded as a thing:\n{}".format(thing))
if 'loadAlways' in info:
for thing in info['loadAlways']:
#print(type(thing))
if isinstance(thing, _gt.Thing):
nextThing = level.addThing(GameMap.resolvePrefab(prefabs, thing), prefabs, nextThing)
if ((thing.thingType == 'x' and not hasKnownEntrance) or thing.thingType == 'a') and prevMap == thing.exitid:
level.playerStart = (thing.x, thing.y)
hasKnownEntrance = True
elif isinstance(thing, Singleton):
if singletons != None:
single = singletons[thing.name]
single.x, single.y = thing.x, thing.y
single.prevx, single.prevy = thing.x, thing.y
nextThing = level.addThing(single, prefabs, nextThing)
else:
raise MapError("Non-thing loaded as a thing:\n{}".format(thing))
return nextThing
# stuff the gameshell itself might use
def addThing(self, thing, prefabs = None, nextThing = 0, persist = False):
if thing == None: # it must be a singleton
return nextThing
#if thing.name in self.thingNames: # resolved
# raise ValueError("Cannot have two objects named {0}.".format(thing.name))
if thing.thingID == -1: # This is to ensure that we don't double up IDs.
thing.thingID = nextThing
nextThing += 1
# Some things, like containers, have other things as custom values,
# so they need IDs as well.
# Let's only add them if they weren't already loaded.
if thing.thingType in 'iun':
nextThing = GameMap.addThingRecursive(thing.customValues, prefabs, nextThing)
if thing.thingType == 'n':
for i in thing.tempInventory:
if i.thingID == -1:
i.thingID = nextThing
nextThing = GameMap.addThingRecursive(i.customValues, prefabs, nextThing + 1)
thing.addThing(i)
del thing.tempInventory
pos = self.coordsToInt(thing.x, thing.y)
if pos not in self.thingPos:
self.thingPos[pos] = [thing.thingID]
else:
self.thingPos[pos].append(thing.thingID)
if thing.name not in self.thingNames:
self.thingNames[thing.name] = [thing.thingID]
else:
self.thingNames[thing.name].append(thing.thingID)
self.things[thing.thingID] = thing
if persist:
self.persistent.append(thing.thingID)
return nextThing
@staticmethod
def addThingRecursive(container, prefabs = None, nextThing = 0):
if isinstance(container, _gt.Thing):
if container.thingID == -1:
container.thingID = nextThing
nextThing = GameMap.addThingRecursive(container.customValues, prefabs, nextThing)
return nextThing + 1
else:
return nextThing
elif isinstance(container, dict):
for i in container:
if isinstance(container[i], _gt.Prefab):
container[i] = GameMap.resolvePrefab(prefabs, container[i])
nextThing = GameMap.addThingRecursive(GameMap.resolvePrefab(prefabs, container[i]), prefabs, nextThing)
return nextThing
elif isinstance(container, list):
for i in range(len(container)):
if isinstance(container[i], _gt.Prefab):
container[i] = GameMap.resolvePrefab(prefabs, container[i])
nextThing = GameMap.addThingRecursive(container[i], prefabs, nextThing)
return nextThing
else:
return nextThing
def getThing(self, **kwargs):
if 'name' in kwargs:
return self.getThingByName(kwargs['name'])
elif 'thingID' in kwargs:
return self.getThingByID(kwargs['thingID'])
elif 'pos' in kwargs:
return self.getThingAtPos(kwargs['pos'])
elif 'coords' in kwargs:
return self.getThingAtCoords(kwargs['coords'][0], kwargs['coords'][1])
else:
raise ValueError('Thing cannot be found by {}.'.format(str(kwargs)))
def removeThing(self, **kwargs):
if 'name' in kwargs:
return self.removeThingByName(kwargs['name'])
elif 'thingID' in kwargs:
return self.removeThingByID(kwargs['thingID'])
elif 'pos' in kwargs:
return self.removeThingAtPos(kwargs['pos'])
elif 'coords' in kwargs:
return self.removeThingAtCoords(kwargs['coords'][0], kwargs['coords'][1])
else:
raise ValueError('Thing cannot be found by {}.'.format(str(kwargs)))
def path(self, x1, y1, loc, closeEnough = True):
startThing = self.getThingAtCoords(x1, y1)
#if not closeEnough:
# if startThing and not startThing.passable:
# return -1, [], -1 # meaning you can't get there
dist, prev, endPoint = self.dijkstra(x1, y1, loc, closeEnough)
numVertex = self.dimensions[0] * self.dimensions[1]
if endPoint > -1 and dist[endPoint] < numVertex + 1:
pathList = [endPoint]
nextPoint = prev[endPoint]
while nextPoint != -1:
pathList.append(nextPoint)
nextPoint = prev[nextPoint]
pathList.reverse()
return dist[endPoint], pathList[1:], endPoint
else:
return -1, [], -1 # meaning you can't get there
def dijkstra(self, x1, y1, loc = None, closeEnough = True):
"""Uses Dijkstra's Algorithm to find the shortest path from (x1, y1) to (x2, y2)
The closeEnough parameter will create a path that lands beside the source if
necessary. The loc parameter is an optional locus which will cause the function
to return once it finds a point that's in the locus."""
endPoint = -1 # until one matches the locus, which it might not.
numVertex = self.dimensions[0] * self.dimensions[1]
dist = [numVertex + 1 for i in range(numVertex)]
prev = [-1 for i in range(numVertex)]
# validation
if loc == None or not self.__validateCoords(x1, y1):
# start point is out-of-bounds or there is no destination
return dist, prev, endPoint
# first test to see that the start point is passable
startThing = self.getThingAtCoords(x1, y1)
startPoint = self.coordsToInt(x1, y1)
dist[startPoint] = 0
queue = []
heapq.heappush(queue, (dist[startPoint], startPoint))
while len(queue) > 0:
u = heapq.heappop(queue)[1]
if loc != None and self.intToCoords(u) in loc:
if endPoint == -1 or dist[u] < dist[endPoint]:
endPoint = u
#print(f"endPoint: {endPoint}; Reason: in locus")
for v in self.mapGraph[u]:
thing = self.getThingAtPos(v)
if thing and not thing.passable:
if closeEnough and self.intToCoords(v) in loc and (endPoint == -1 or dist[u] < dist[endPoint]):
endPoint = u
#print(f"endPoint: {endPoint}; Reason: good enough")
else:
continue
tempDist = dist[u] + 1
if tempDist < dist[v]:
dist[v] = tempDist
if dist[u] != numVertex + 1:
prev[v] = u
heapq.heappush(queue, (dist[v], v))
return dist, prev, endPoint
#if dist[endPoint] < numVertex + 1:
# return dist[endPoint], prev
#else:
# return -1, [] # meaning you can't get there
def lineOfSight(self, x1, y1, x2, y2):
"""Test for line of signt from one tile to another."""
if not (self.__validateCoords(x1, y1) and self.__validateCoords(x2, y2)):
return False
# Trivial case first:
if abs(x1 - x2) <= 1 and abs(y1 - y2) <= 1:
return True
# Common case second:
lst = list(_gl.LineLocus(x1, y1, x2, y2, False))[1:-1]
# Here is where we actually check:
for space in lst:
if not self.isPassable(self.coordsToInt(*space)):
return False
return True
def isPassable(self, x, y = -1):
if y == -1:
if not self.__validatePos(x):
return False
pos = x
x, y = self.intToCoords(x)
else:
if not self.__validateCoords(x, y):
return False
pos = self.coordsToInt(x, y)
if self.mapMatrix[y][x][0] == 'w':
return False
thingsInSpace = self.getThingsAtPos(pos)
for thing in thingsInSpace:
if not thing.passable:
return False
return True
def __validateCoords(self, x: int, y: int) -> bool:
return x >= 0 and x < self.dimensions[0] and y >= 0 and y < self.dimensions[1]
def __validatePos(self, pos: int) -> bool:
return pos >= 0 and pos < self.dimensions[0] * self.dimensions[1]
@staticmethod
def __coordsToInt(x, y, width):
return x + y * width
def coordsToInt(self, x: int, y: int, width = -1):
if width < 0:
return x + y * self.dimensions[0]
else:
return x + y * width
def intToCoords(self, pos: int):
return pos % self.dimensions[0], int(pos / self.dimensions[0])
def getThingAtCoords(self, x, y):
return self.getThingAtPos(self.coordsToInt(x, y))
def getThingsAtCoords(self, x, y):
return self.getThingsAtPos(self.coordsToInt(x, y))
def getThingAtPos(self, pos):
if pos in self.thingPos:
return self.things[self.thingPos[pos][0]]
else:
return None
def getThingsAtPos(self, pos):
if pos in self.thingPos:
return [self.things[i] for i in self.thingPos[pos]]
else:
return []
def getThingByName(self, name):
if name in self.thingNames:
return self.things[self.thingNames[name][0]]
else:
return None
def getThingsByName(self, name):
if name in self.thingNames:
ret = []
for i in self.thingNames[name]:
ret.append(self.things[i])
return ret
else:
return []
def getThingByID(self, thingID):
if thingID in self.things:
return self.things[thingID]
else:
return None
def removeThingByThing(self, thing):
if thing != None:
oldPos = self.coordsToInt(thing.x, thing.y)
if oldPos in self.thingPos:
self.thingPos[oldPos].remove(thing.thingID)
if len(self.thingPos[oldPos]) == 0:
del self.thingPos[oldPos]
oldName = thing.name
if oldName in self.thingNames:
self.thingNames[oldName].remove(thing.thingID)
if len(self.thingNames[oldName]) == 0:
del self.thingNames[oldName]
if thing.thingID in self.persistent:
self.persistent.remove(thing.thingID)
del self.things[thing.thingID]
return thing
else:
return None
def removeThingAtCoords(self, x, y):
return self.removeThingAtPos(self.coordsToInt(x, y))
def removeThingsAtCoords(self, x, y):
return self.removeThingsAtPos(self.coordsToInt(x, y))
def removeThingAtPos(self, pos):
if pos in self.thingPos:
return self.removeThingByThing(self.getThingAtPos(pos))
else:
return None
def removeThingsAtPos(self, pos):
if pos in self.thingPos:
ret = []
for i in self.thingPos[pos]:
ret.append(self.removeThingByThing(self.things[i]))
return ret
else:
return []
def removeThingByName(self, name):
if name in self.thingNames:
return self.removeThingByThing(self.getThingByName(name))
else:
return None
def removeThingsByName(self, name):
if name in self.thingNames:
ret = []
for i in self.thingNames[name]:
ret.append(self.removeThingByThing(self.things[i]))
return ret
else:
return []
def removeThingByID(self, thingID):
if thingID in self.things:
return self.removeThingByThing(self.things[thingID])
else:
return None
def moveThing(self, thing, x, y = -1):
newPos = x
if y != -1:
newPos = self.coordsToInt(x, y)
else:
x, y = self.intToCoords(x)
if thing != None:
if thing.x == x and thing.y == y:
return # it's already there, so don't do anything.
oldPos = self.coordsToInt(thing.x, thing.y)
if oldPos in self.thingPos:
self.thingPos[oldPos].remove(thing.thingID)
if len(self.thingPos[oldPos]) == 0:
del self.thingPos[oldPos]
if newPos not in self.thingPos:
self.thingPos[newPos] = [thing.thingID]
else:
self.thingPos[newPos].append(thing.thingID)
relPlayerx, relPlayery = thing.playerx - thing.x, thing.playery - thing.y
thing.prevx, thing.prevy = thing.x, thing.y
thing.x, thing.y = x, y
thing.playerx, thing.playery = thing.x + relPlayerx, thing.y + relPlayery
else:
raise MapError("There is nothing to move.")
class LoSLocus(_gl.Locus):
"""A locus that defines all points within line-of-sight of a given points."""
def __init__(self, x, y, level):
self.x = x
self.y = y
self.level = level
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
return self.level.lineOfSight(x, y, self.x, self.y)
def __iter__(self):
ret = [(self.x, self.y)]
mat = [[False for i in range(self.level.y)] for j in range(self.level.x)]
mat[self.x][self.y] = True
fringe = [(self.x, self.y + 1), (self.x + 1, self.y), (self.x, self.y - 1), (self.x - 1, self.y),
(self.x + 1, self.y + 1), (self.x + 1, self.y - 1), (self.x - 1, self.y - 1), (self.x - 1, self.y + 1)]
while len(fringe) > 0:
point = fringe.pop(0)
check = []
if abs(point[0] - self.x) > abs(point[1] - self.y):
if point[0] > self.x:
check.append((point[0] - 1, point[1]))
if point[1] > self.y:
check.append((point[0] - 1, point[1] - 1))
elif point[1] < self.y:
check.append((point[0] - 1, point[1] + 1))
else:
check.append((point[0] + 1, point[1]))
if point[1] > self.y:
check.append((point[0] + 1, point[1] - 1))
elif point[1] < self.y:
check.append((point[0] + 1, point[1] + 1))
elif abs(point[0] - self.x) < abs(point[1] - self.y):
if point[1] > self.y:
check.append((point[0], point[1] - 1))
if point[0] > self.x:
check.append((point[0] - 1, point[1] - 1))
elif point[0] < self.x:
check.append((point[0] + 1, point[1] - 1))
else:
check.append((point[0], point[1] + 1))
if point[0] > self.x:
check.append((point[0] - 1, point[1] + 1))
elif point[0] < self.x:
check.append((point[0] + 1, point[1] + 1))
else:
if point[0] > self.x:
if point[1] > self.y:
check.append((point[0] - 1, point[1] - 1))
else:
check.append((point[0] - 1, point[1] - 1))
else:
if point[1] > self.y:
check.append((point[0] + 1, point[1] - 1))
else:
check.append((point[0] + 1, point[1] - 1))
status = [mat[i[0]][i[1]] for i in check]
addIf = False
if True in status:
if False in status:
addIf = self.level.lineOfSight(point[0], point[1], self.x, self.y)
else:
addIf = True
if addIf:
mat[point[0]][point[1]] = self.level.isPassable(*point)
ret.append(point)
return iter(ret)
# |\_/|
# /0 0\
# \o/

View file

@ -0,0 +1,357 @@
# gamesequence.py
"""This contains the functions and classes necessary to parse and execute the scripting language.
Classes:
- SequenceError: Derived from RuntimeError.
- ScriptBreak: Represent a 'break' statement.
- ScriptEnvironment: Represent the environment of a script.
Functions:
- getValueFromString: Translate a literal or variable to a value.
- compareValues: compare values for a conditional statement.
- ifScript: Execute an 'if' statement.
- getCustomValue: Get the value of a global variable.
- setCustomValue: Set the value of a global variable.
- delCustomValue: Delete a global variable.
- runScript: Execute a script.
- parseScript: Parse a script.
"""
import re as _re
class SequenceError(RuntimeError):
"""Derived from RuntimeError.
Raise whenever there is a runtime execution problem while parsing or executing a script.
"""
pass
class ScriptBreak(object):
"""Class created when a 'break' statement is read."""
def __init__(self, value):
"""Create a script break object with the value returned from the script broken from."""
self.value = value
class ScriptEnvironment(object):
"""Represent the environment of a script.
This contains a dictionary of all global variables, as well as local variables."""
def __init__(self, globalVars: dict, localVars: dict):
"""Create a script environment.
Global vars will survive beyond the end of the script.
Local vars are all dropped at the end of a script."""
if not isinstance(globalVars, dict):
raise TypeError("Global variables must be in a dictionary.")
if not isinstance(globalVars, dict):
raise TypeError("Local variables must be in a dictionary.")
self.globalVars = globalVars
self.localVars = localVars
def getValueFromString(arg: str, env: ScriptEnvironment):
"""Translate a literal or variable name into a value.
arg should be a string representing a literal or identifier.
env should be a ScriptEnvironment.
Return the value described by the first literal or variable name."""
val = None
# We test for a valid identifier here, before all the if-elif starts.
validIdent = _re.match(r'[_A-Za-z][_0-9A-Za-z]*', arg)
if arg[0] in '"\'' and arg[-1] == arg[0]:
# The argument is a string literal.
val = arg[1:-1]
elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None:
# The argument is a number.
if '.' in arg:
# The argument is a float.
val = float(arg)
else:
# The argument is an int.
val = int(arg)
elif arg.casefold() == 'true':
# The argument is the boolean value 'true'.
val = True
elif arg.casefold() == 'false':
# The argument is the boolean value 'false'.
val = False
elif validIdent != None and env != None:
# The argument is a variable name.
group = validIdent.group()
if group in env.localVars:
# The variable is local.
val = env.localVars[group]
elif group in env.globalVars:
# The variable is global.
val = env.globalVars[group]
else:
return False # for if statements; if a variable doesn't exist, should evaluate to False
# evaluate all values of all indecies
openBracket = validIdent.end()
if openBracket < len(arg):
if arg[openBracket] == '[':
ptr = openBracket
depth = 0
while ptr < len(arg):
if depth == 0 and arg[ptr] != '[':
raise SequenceError('Invalid value syntax: {}'.format(arg))
if arg[ptr] == '[':
depth += 1
elif arg[ptr] == ']':
depth -= 1
if depth == 0:
index = getValueFromString(arg[openBracket+1:ptr], env)
if index in val:
val = val[index]
else:
return False
openBracket = ptr + 1
ptr += 1
if depth != 0:
#depth should always == 0 at this point
raise SequenceError('Mismatched number of open and close brackets: {}'.format(arg))
else:
raise SequenceError('Invalid value syntax: {}'.format(arg))
else:
raise SequenceError('Invalid argument to getValueFromString: {}'.format(arg))
return val
def compareValues(args: list, env: ScriptEnvironment):
"""Generalized comparisons, may eventually be extended to other operators"""
if len(args) == 1:
return bool(getValueFromString(args[0], env))
elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'):
lval = getValueFromString(args[0], env)
operator = args[1]
rval = getValueFromString(args[2], env)
if operator == '==':
return lval == rval
elif operator == '!=':
return lval != rval
elif operator == '<=':
return lval <= rval
elif operator == '>=':
return lval >= rval
elif operator == '<':
return lval < rval
elif operator == '>':
return lval > rval
elif operator == 'in':
return lval in rval
else:
raise SequenceError("Condition cannot be evaluated: {}".format(' '.join(args)))
def ifScript(args, env: ScriptEnvironment, externalScripts: dict):
"""If statement: if [not] value [op value] script"""
if len(args) < 2:
raise GameError('Incomplete If statement: if {}'.format(' '.join(args)))
inverse = False
ret = False
if args[0] == 'not':
inverse = True
args.pop(0)
if len(args) < 2:
raise GameError('Incomplete If statement: if {}'.format(' '.join(args)))
# evaluate condition
if len(args) > 1 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'):
if len(args) < 4:
raise SequenceError('Incomplete If statement: if {}'.format(' '.join(args)))
ret = compareValues(args[:3], env)
args = args[3:]
else:
ret = bool(getValueFromString(args[0], env))
args = args[1:]
if inverse:
ret = not ret
# if condition is true, evaluate further
if ret:
if isinstance(args[-1], list):
return runScript(args[-1], env, externalScripts)
else:
return runScript([args], env, externalScripts)
def getCustomValue(args, env: dict):
if env == None:
raise SequenceError("Cannot get a value from an empty environment.")
return getValueFromString(args[0], env)
def setCustomValue(args, env: dict):
"""takes [customValue, op, value]"""
if env == None:
raise SequenceError("Cannot set a value from an empty environment.")
scope = env.localVars
if len(args) > 0 and args[0] == 'global':
scope = env.globalVars
args.pop(0)
if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='):
raise SequenceError('Arguments are not fit for the setCustomValue script.')
# This next line allows cvmod to use values in the thing's customValues dict,
# but doing this means that accessing a game CV requires setting it to a local var.
val = getValueFromString(args[2], env)
# set the value to a default value (0, 0.0, '', etc.) if not yet assigned
if args[0] not in scope and args[1] != '=':
if isinstance(val, int):
scope[args[0]] = 0
elif isinstance(val, float):
scope[args[0]] = 0.0
elif isinstance(val, str):
scope[args[0]] = ''
elif isinstance(val, bool):
scope[args[0]] = False
# Beyond this point are types that can only be found from other customValues
elif isinstance(val, tuple):
scope[args[0]] = ()
elif isinstance(val, list):
scope[args[0]] = []
elif isinstance(val, dict):
scope[args[0]] = {}
else:
raise SequenceError("Operation not supported for unassigned custom values of type {}: {}"
.format(type(val), args[1]))
# done parsing, evaluate
if args[1] == '=':
scope[args[0]] = val
elif args[1] == '+=':
scope[args[0]] += val
elif args[1] == '-=':
scope[args[0]] -= val
elif args[1] == '*=':
scope[args[0]] *= val
elif args[1] == '/=':
scope[args[0]] /= val
elif args[1] == '%=':
scope[args[0]] %= val
elif args[1] == '//=':
scope[args[0]] //= val
elif args[1] == '**=':
scope[args[0]] **= val
elif args[1] == 'b=':
scope[args[0]] = bool(val)
elif args[1] == '!=':
scope[args[0]] = not bool(val)
elif args[1] == '|=':
scope[args[0]] |= val
elif args[1] == '&=':
scope[args[0]] &= val
elif args[1] == '^=':
scope[args[0]] ^= val
return scope[args[0]]
def delCustomValue(args, env: ScriptEnvironment):
"""To clean up after a map."""
if env == None:
raise SequenceError("Cannot delete a value from an empty environment.")
for i in args:
if i in env.globalVars:
del(env.globalVars[i])
_requireEnv = {'get' : getCustomValue, 'set' : setCustomValue, 'del' : delCustomValue}
_requireScripts = {'if' : ifScript}
def runScript(script: list, env: ScriptEnvironment, externalScripts: dict):
"""run a script"""
ret = False
for line in script:
if len(line) == 0:
# empty line
continue
elif line[0].casefold() == 'playercmd':
# run a player command
self.getIO('playercmd')(line[1:])
elif line[0].casefold() in _requireScripts:
ret = _requireScripts[line[0]](line[1:], env, externalScripts)
elif line[0].casefold() in _requireEnv:
ret = _requireEnv[line[0]](line[1:], env)
elif line[0].casefold() == 'break':
# exit early
return ScriptBreak(ret)
elif line[0] in externalScripts:
# run a script
ret = externalScripts[line[0]](line[1:])
else:
# conditional evaluation
return compareValues(line, env)
if isinstance(ret, ScriptBreak):
return ret
# We specifically want the return value of the last line executed.
return ret
def parseScript(instr: str, envGlobal: dict, externalScripts: dict):
"""parses then runs a script."""
#print('parseScript called with {}.'.format(instr))
if instr == '':
return # nothing to be done.
literalStr = list(instr)
inQuotes = False
script = []
stack = []
ret = []
argStart = 0
c = 0
l = 0
while c < len(instr):
if inQuotes:
if instr[c] == '"':
if instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
inQuotes = False
del literalStr[l]
l -= 1
else:
if instr[c] == '"': # quoted string
if l > 0 and instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
inQuotes = True
del literalStr[l]
l -= 1
elif instr[c] in ' \t\n;}{':
if l > 0 and instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
if instr[c] == ';':
if len(ret) > 0:
script.append(ret)
ret = []
elif instr[c] == '{': # block
stack.append(script)
script.append(ret)
script = []
ret.append(script)
ret = []
del literalStr[l]
l -= 1
elif instr[c] == '}': # close block
if len(ret) > 0:
script.append(ret)
ret = []
script = stack.pop()
del literalStr[l]
l -= 1
argStart = l + 1
c += 1
l += 1
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
if len(ret) > 0:
script.append(ret)
env = ScriptEnvironment(envGlobal, {})
ret = runScript(script, env, externalScripts)
if isinstance(ret, ScriptBreak):
ret = ret.value
return ret

770
src/gameshell/gamethings.py Normal file
View file

@ -0,0 +1,770 @@
#gamethings.py
"""Standard thing classes.
Classes:
- ThingGraphic: Represents how thing is to be visualized.
- Thing: The abstract base class for every thing.
- Observer: The abstract base class for things that listen to events.
- Item: A thing that can exist in a character's inventory.
- Useable: A thing that can be used by a character.
- Character: A thing that represents a character.
- NPC: A character that is not controlled by the player.
- Door: A thing that sometimes blocks paths.
- MapExit: A technical thing that marks a map transition.
"""
import ruamel.yaml
from ruamel.yaml.comments import CommentedMap
class ThingGraphic(object):
"""Represent how a thing is to be visualized.
Background color, foreground color, and shape are represented.
This could hypothetically be extended to hold sprites, textures or models."""
def __init__(self, background: str, foreground: str, shape: str):
"""Create a graphic for a thing.
The background and foregrond must be strings with color hex representations.
For instance: '#00FF00' means bright green.
The shape must be a string containing exactly one of the following shapes:
o: circle
x: cross
-: horizontal line
|: vertical line
#: square
^: triangle
A graphic may be invalid.
"""
if not isinstance(background, str):
raise TypeError("Background must be a string of form '#[0-9A-Fa-f]{6}'.")
if not isinstance(foreground, str):
raise TypeError("Foreground must be a string of form '#[0-9A-Fa-f]{6}'.")
if not isinstance(shape, str):
raise TypeError("Shape must be a string of form '[-|ox#^]'.")
self.background = background
self.foreground = foreground
self.shape = shape
class Thing(object):
def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None, **kwargs):
self.thingType = thingType
self.name = name
self.description = description
self.x = x
self.y = y
self.playerx = x
self.playery = y
self.prevx = x # if an area gets double-occupied, a thing can get pushed back.
self.prevy = y
if playerx:
self.playerx = playerx
if playery:
self.playery = playery
self.flags = flags
self.graphic = ThingGraphic('clear', '#7F7F7F', ' ')
self.thingID = -1 # ID not assigned
def __str__(self):
"""__str__ is used for look."""
return self.description
def __eq__(self, other):
if not isinstance(other, Thing):
return False
return self.name == other.name
@property
def passable(self):
return bool(self.flags & 1)
@property
def talkable(self):
return bool(self.flags & 2)
@property
def lookable(self):
return bool(self.flags & 4)
@property
def takeable(self):
return bool(self.flags & 8)
@property
def useable(self):
return bool(self.flags & 16)
def fromPrefab(self, prefab):
"""This only exists as a catch-all in case an unsupported thing type is prefabed."""
parts = prefab.details
# set default values for optional arguments
name = self.name
description = self.description
playerx = prefab.x + (self.playerx - self.x)
playery = prefab.y + (self.playery - self.y)
bgc = self.graphic.background
fgc = self.graphic.foreground
shape = self.graphic.shape
if parts != None:
if 'name' in parts:
name = parts['name']
if 'description' in parts:
description = parts['description']
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
graphic = ThingGraphic(bgc, fgc, shape)
return Thing(self.thingType, name, prefab.x, prefab.y, description, self.flags, playerx, playery)
class Prefab(Thing):
"""Basically a placeholder for a predefined prefab. This is for reading in YAML."""
yaml_flag = u'!Prefab'
defaultGraphic = ThingGraphic('clear', 'clear', 'x')
def __init__(self, name: str, x: int, y: int, **kwargs):
super(Prefab, self).__init__('z', name, x, y, '', 1) # 'p' is already taken by the player.
self.details = None # This is so that custom overrides can be made for specific instances.
if "details" in kwargs:
self.details = kwargs["details"]
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'details': self.details}
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
mdetails = None
if 'details' in parts:
mdetails = dict(parts['details'])
return cls(parts['name'], parts['location'][0], parts['location'][1], details = mdetails)
class Observer(Thing):
"""ABC for things that have a dict of events that they should listen to."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.behaviors = {} # {the name of the event : (behaviorQueue priority, the name of the behavior)}
self.busy = False # Prevents a new behavior until the current one is finished
self.behaviorQueue = [] # If you can't make it perfect, then make it adjustable.
# The behavior queue is for behaviors that need to execute once the current action is finished.
# If it's not considered important enough (behaviors[event][0] < 0), it's dropped.
class Item(Thing):
yaml_flag = u'!Item'
defaultGraphic = ThingGraphic('clear', '#00BF00', '^')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic):
super(Item, self).__init__('i', name, x, y, description, 13)
self.useFunc = useFunc
self.useOnFunc = useOnFunc
self.customValues = customValues
self.ranged = ranged
self.graphic = graphic
def use(self):
pass
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic.background != Item.defaultGraphic.background:
graphic['bgc'] = node.graphic.background
if node.graphic.foreground != Item.defaultGraphic.forground:
graphic['fgc'] = node.graphic.foreground
if node.graphic.shape != Item.defaultGraphic.shape:
graphic['shape'] = node.graphicshape
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if node.useOnFunc != '':
ret['useOnFunc'] = node.useOnFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.ranged:
ret['ranged'] = node.ranged
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
bgc = Item.defaultGraphic.background
fgc = Item.defaultGraphic.foreground
shape = Item.defaultGraphic.shape
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = ThingGraphic(bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'useOnFunc' in parts:
useOnFunc = parts['useOnFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'ranged' in parts:
ranged = parts['ranged']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, useOnFunc, customValues, ranged, graphic)
def use(self, user, gameBase, args) -> float:
pass
def useOn(self, user, gameBase, thing, args) -> float:
pass
def fromPrefab(self, prefab):
parts = prefab.details
# set default values for optional arguments
name = self.name
useFunc = self.useFunc
useOnFunc = self.useOnFunc
customValues = dict(self.customValues)
ranged = self.ranged
bgc = self.graphic.background
fgc = self.graphic.foreground
shape = self.graphic.shape
description = self.description
if parts:
if 'name' in parts:
name = parts['name']
if 'description' in parts:
description = parts['description']
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'useOnFunc' in parts:
useOnFunc = parts['useOnFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'ranged' in parts:
ranged = parts['ranged']
graphic = ThingGraphic(bgc, fgc, shape)
return Item(name, prefab.x, prefab.y, description, useFunc, useOnFunc, customValues, ranged, graphic)
class Useable(Thing):
yaml_flag = u'!Useable'
defaultGraphic = ThingGraphic('clear', '#0000FF', '#')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic):
super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery)
self.useFunc = useFunc
self.customValues = customValues
self.graphic = graphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic.background
if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic.foreground
if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic.shape
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
customValues = {}
playerx, playery = parts['location']
bgc = Useable.defaultGraphic.background
fgc = Useable.defaultGraphic.foreground
shape = Useable.defaultGraphic.shape
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = ThingGraphic(bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, customValues, playerx, playery, graphic)
def use(self, user, gameBase, args) -> float:
pass
def fromPrefab(self, prefab):
parts = prefab.details
# set default values for optional arguments
name = self.name
description = self.description
useFunc = self.useFunc
customValues = dict(self.customValues)
playerx = prefab.x + (self.playerx - self.x)
playery = prefab.y + (self.playery - self.y)
bgc = self.graphic.background
fgc = self.graphic.foreground
shape = self.graphic.shape
if parts != None:
if 'name' in parts:
name = parts['name']
if 'description' in parts:
description = parts['description']
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
graphic = ThingGraphic(bgc, fgc, shape)
return Useable(name, prefab.x, prefab.y, description, useFunc, customValues, playerx, playery, graphic)
class Character(Thing):
defaultGraphic = ThingGraphic('clear', '#000000', 'o')
def __init__(self, inventory: dict, customValues: dict, graphic = defaultGraphic, **kwargs):
super(Character, self).__init__(**kwargs)
if inventory == None:
inventory = {} # create a new dict for the inventory.
# This couldn't be in the NPC constructor because
# then all characters would share a reference to
# the same empty inventory.
self.__inventory = inventory
self.customValues = customValues
self.graphic = graphic
self.thingNames = {} #{str: int}
# set up inventory shtuff
for i in self.__inventory:
if self.__inventory[i].name in self.thingNames:
self.thingNames[self.__inventory[i].name].append(i)
else:
self.thingNames[self.__inventory[i].name] = [i]
def addThing(self, thing):
if not isinstance(thing, Item):
raise TypeError("Only items can be added to a character's inventory.")
self.__inventory[thing.thingID] = thing
if thing.name in self.thingNames:
self.thingNames[thing.name].append(thing.thingID)
else:
self.thingNames[thing.name] = [thing.thingID]
def getThingByID(self, thingID):
if thingID in self.__inventory:
return self.__inventory[thingID]
else:
return None
def getThingByName(self, name):
if name in self.thingNames:
return self.__inventory[self.thingNames[name][0]]
else:
return None
def removeThingByID(self, thingID):
ret = self.__inventory[thingID]
self.thingNames[ret.name].remove(thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[thingID]
return ret
def removeThingByName(self, name):
ret = self.getThingByName(name)
self.thingNames[ret.name].remove(ret.thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[ret.thingID]
return ret
def removeThing(self, ret):
self.thingNames[ret.name].remove(ret.thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[ret.thingID]
return ret
@property
def inventory(self):
"""Get the inventory as a list."""
return list(self.__inventory.values())
class NPC(Character, Observer):
yaml_flag = u'!NPC'
defaultGraphic = ThingGraphic('clear', '#000000', 'o')
def __init__(self, behaviors: dict, tempInventory: list, **kwargs):
if 'graphic' not in kwargs:
kwargs['graphic'] = PlayerCharacter.defaultGraphic
super(NPC, self).__init__(thingType = 'n', inventory = {}, flags = 6, **kwargs)
self.behaviors = behaviors
self.behaveEvent = None
self.tempInventory = tempInventory # should be deleted once NPC is loaded
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y),
'description': node.description, 'behaviors': node.behaviors}
# save graphic
graphic = {}
if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic.background
if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic.foreground
if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic.shape
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if len(node.inventory) > 0:
ret['inventory'] = node.inventory
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
minventory = []
mcustomValues = {}
mplayerx, mplayery = parts['location']
bgc = NPC.defaultGraphic.background
fgc = NPC.defaultGraphic.foreground
shape = NPC.defaultGraphic.shape
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
mgraphic = ThingGraphic(bgc, fgc, shape)
# load use functions
if 'inventory' in parts:
inventory = parts['inventory']
if 'customValues' in parts:
mcustomValues = dict(parts['customValues'])
for v in mcustomValues:
if isinstance(mcustomValues[v], tuple):
mcustomValues[v] = list(mcustomValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(name = parts['name'], x = parts['location'][0], y = parts['location'][1],
description = parts['description'], behaviors = parts['behaviors'], tempInventory = minventory, customValues = mcustomValues,
playerx = mplayerx, plyery = mplayery, graphic = mgraphic)
def fromPrefab(self, prefab):
parts = prefab.details
# set default values for optional arguments
mname = self.name
mdescription = self.description
minventory = list(self.tempInventory)
mcustomValues = dict(self.customValues)
mbehaviors = dict(self.behaviors)
mplayerx = prefab.x + (self.playerx - self.x)
mplayery = prefab.y + (self.playery - self.y)
bgc = self.graphic.background
fgc = self.graphic.foreground
shape = self.graphic.shape
if prefab != None:
if 'name' in parts:
mname = parts['name']
if 'description' in parts:
mdescription = parts['description']
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
# load use functions
if 'inventory' in parts:
inventory = parts['inventory']
if 'customValues' in parts:
mcustomValues = dict(parts['customValues'])
for v in mcustomValues:
if isinstance(mcustomValues[v], tuple):
mcustomValues[v] = list(mcustomValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
if 'behaviors' in parts:
mbehaviors = dict(parts['behaviors'])
mgraphic = ThingGraphic(bgc, fgc, shape)
return NPC(name = mname, x = prefab.x, y = prefab.y, description = mdescription, behaviors = mbehaviors, tempInventory = minventory, customValues = mcustomValues,
playerx = mplayerx, plyery = mplayery, graphic = mgraphic)
class Door(Thing):
yaml_flag = u'!Door'
defaultGraphic = ThingGraphic('clear', '#7F3F00', '#')
def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic):
self.descBase = description
if description == None:
if locked:
description = "The {0} is locked.".format(name)
else:
description = "The {0} is unlocked.".format(name)
else:
if locked:
description += " It is locked."
else:
description += " It is unlocked."
flags = 1
if locked:
flags = 0
super(Door, self).__init__('d', name, x, y, description, flags)
self.key = key
self.graphic = graphic
def lock(self, key = None):
if key == self.key:
self.flags ^= 1
#self.passable = not self.passable
if self.descBase == None:
if self.passable:
self.description = "The {0} is unlocked.".format(self.name)
else:
self.description = "The {0} is locked.".format(self.name)
else:
if self.passable:
self.description += " It is unlocked."
else:
self.description += " It is locked."
return True
return False
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y)}
# save graphic
graphic = {}
if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic.background
if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic.foreground
if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic.shape
if len(graphic) > 0:
ret['graphic'] = graphic
# save door state
if node.passable:
ret['locked'] = not node.passable
if node.descBase != None:
ret['description'] = node.descBase
if node.key != None:
ret['key'] = node.key
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
description = None
locked = False
key = None
bgc = Door.defaultGraphic.background
fgc = Door.defaultGraphic.foreground
shape = Door.defaultGraphic.shape
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = ThingGraphic(bgc, fgc, shape)
# load door state
if 'description' in parts:
description = parts['description']
if 'locked' in parts:
locked = parts['locked']
if 'key' in parts:
key = parts['key']
return cls(parts['name'], parts['location'][0], parts['location'][1],
locked, description, key, graphic)
class MapExit(Thing):
yaml_flag = u'!MapExit'
defaultGraphic = ThingGraphic('clear', '#FF0000', 'x')
def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic):
description = name
if prefix:
description = "{0} {1}".format(prefix, name)
super(MapExit, self).__init__('x', name, x, y, description, 5)
self.exitid = exitid
self.destination = destination
self.prefix = prefix
self.onUse = onUse
self.key = key
self.graphic = graphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination}
# save graphic
graphic = {}
if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic.background
if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic.foreground
if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic.shape
if len(graphic) > 0:
ret['graphic'] = graphic
if node.prefix != None:
ret['prefix'] = node.prefix
if node.onUse != '':
ret['onUse'] = node.onUse
if node.key != True:
ret['key'] = node.key
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
prefix = None
onUse = ''
key = True
bgc = MapExit.defaultGraphic.background
fgc = MapExit.defaultGraphic.foreground
shape = MapExit.defaultGraphic.shape
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = ThingGraphic(bgc, fgc, shape)
if 'prefix' in parts:
prefix = parts['prefix']
if 'onUse' in parts:
onUse = parts['onUse']
if 'key' in parts:
key = parts['key']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['id'], parts['destination'], prefix, onUse, key, graphic)
class MapEntrance(Thing):
yaml_flag = u'!MapEntrance'
defaultGraphic = ThingGraphic('clear', 'clear', 'x')
# no graphic - should not be drawn
def __init__(self, x: int, y: int, exitid: int, name = None):
if name == None:
name = 'entrance {}'.format(exitid)
super(MapEntrance, self).__init__('a', name, x, y, '', 1)
self.exitid = exitid
#self.graphic = MapEntrance.defaultGraphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'location': (node.x, node.y), 'id': node.exitid}
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
return cls(parts['location'][0], parts['location'][1], parts['id'])
class PlayerCharacter(Character):
"""Player object. Cannot be created with yaml."""
defaultGraphic = ThingGraphic('clear', '#0000FF', 'o')
def __init__(self, **kwargs):
if 'name' not in kwargs:
kwargs['name'] == 'You'
if 'graphic' not in kwargs:
kwargs['graphic'] = PlayerCharacter.defaultGraphic
super(PlayerCharacter, self).__init__(thingType = 'p', flags = 5, **kwargs)

104
src/gameshell/gameutil.py Normal file
View file

@ -0,0 +1,104 @@
# gameutil.py
# This is a home for nice helper functions that were useful, but also cluttered
# the GameBase class.
import re as _re
coordRegex = _re.compile(r'(?:(-?[a-zA-Z]+) ?(-?[0-9]+))|(?:(-?[0-9]+),? (-?[0-9]+))|(?:\(([0-9]+), ([0-9]+)\))')
def letterToNumber(letter):
if letter.isdecimal():
return int(letter)
ret = 0
sign = 1
start = 0
for i in range(len(letter)):
if letter[i] == '-':
sign = -sign
start -= 1
elif letter[i].isalpha():
if letter[i].isupper():
ret += (ord(letter[i]) - ord('A')) * (26**(i+start))
else:
ret += (ord(letter[i]) - ord('a')) * (26**(i+start))
else:
return ret * sign
return ret * sign
def numberToLetter(number):
if isinstance(number, str):
return number
ret = ''
sign = ''
if number == 0:
return 'A'
elif number < 0:
sign = '-'
number = -number
while number > 0:
ret += chr(ord('A') + number % 26)
number = int(number / 26)
return sign + ret
def parseCoords(level, args, usePlayerCoords = True, player = None):
"""Takes an argument list, and figures out what it's talking about.
Returns (thing, x, y). "Thing" can be None."""
if level == None:
raise ValueError('Coordinates cannot be parsed because there is no map.')
if len(args) == 0:
return None, -1, -1
x = -1
y = -1
# first, try by name for objects in the map
name = ' '.join(args)
thing = level.getThingByName(name)
if thing != None:
if usePlayerCoords:
return thing, thing.playerx, thing.playery
else:
return thing, thing.x, thing.y
# Second, try by name for objects in the inventory
if player != None:
if name in player.thingNames:
return player.getThingByName(name), -1, -1
# Third, try by location
coordStr = args[0]
if len(args) > 1:
coordStr += ' ' + args[1]
match = coordRegex.match(coordStr)
if match != None: # if coordinates were given
groups = match.groups()
ind = 0
for i in range(len(groups)):
if groups[i] == None or groups[i] == match.group():
continue
else:
ind = i
break
x = letterToNumber(groups[ind])
y = letterToNumber(groups[ind+1])
thing = level.getThingAtCoords(x, y)
if thing != None and usePlayerCoords:
return thing, thing.playerx, thing.playery
else:
return thing, x, y
else: # if a name was given
return None, -1, -1
def startDialog(thing, outstream, dialog):
if 'dialogs' in thing.customValues:
if isinstance(thing.customValues['dialogs'], list):
if 'dialogPtr' in thing.customValues and isinstance(thing.customValues['dialogPtr'], int):
dialog(thing.customValues['dialogs'][thing.customValues['dialogPtr']], thing)
else:
dialog(thing.customValues['dialogs'][0], thing)
elif isinstance(thing.customValues['dialogs'], str):
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 = outstream)

View file

@ -0,0 +1,44 @@
from gamelocus import *
def demo(loc):
print(loc)
allpoints = [i for i in loc]
print("Contains: Iter: ")
for row in range(10):
for col in range(10):
if (col, 9 - row) in loc:
print("##", end = "")
else:
print("..", end = "")
for col in range(10):
if (col, 9 - row) in allpoints:
print("##", end = "")
else:
print("..", end = "")
print()
demo(LineLocus(1, 1, 8, 8))
demo(LineLocus(1, 1, 8, 8, True))
demo(LineLocus(1, 8, 8, 1))
demo(LineLocus(1, 8, 8, 1, True))
demo(LineLocus(1, 3, 8, 6))
demo(LineLocus(1, 3, 8, 6, True))
demo(LineLocus(1, 6, 8, 3))
demo(LineLocus(1, 6, 8, 3, True))
demo(LineLocus(3, 1, 6, 8))
demo(LineLocus(3, 1, 6, 8, True))
demo(LineLocus(1, 5, 8, 5))
demo(LineLocus(1, 5, 8, 5, True))
demo(RectLocus(1, 1, 8, 8))
demo(FilledRectLocus(1, 1, 8, 8))
demo(RectLocus(1, 3, 8, 6))
demo(FilledRectLocus(1, 3, 8, 6))
demo(CircleLocus(4, 4, 4))
demo(CircleLocus(4, 4, 1))
demo(CircleLocus(4, 4, 0.6))
demo(CircleLocus(4, 4, 0.4))
demo(FilledCircleLocus(4, 4, 4))
demo(FilledCircleLocus(4, 4, 1))
demo(FilledCircleLocus(4, 4, 0.6))
demo(FilledCircleLocus(4, 4, 0.4))
demo(SetLocus(((1, 1), (1, 8), (8, 8), (8, 1))))

617
src/gameshell/main.py Normal file
View file

@ -0,0 +1,617 @@
# gameshell.py
from .shell import Shell
from .gamebase import GameBase
import sys as _sys
import os as _os
#import re
import heapq
#import gamemap
from . import gameevents
import textwrap as _tw
from shutil import get_terminal_size as _gts
from . 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)
self.gameTitle = 'Game Shell' # should be changed for actual games
if 'title' in data:
self.gameTitle = data['title']
self.startLevel = 'testing/test1.yml' # should be changed for actual games
if 'startLevel' in data:
self.startLevel = data['startLevel']
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 = '?> '
self.__inGame = False
# register functions
self.registerCommand('load', self.loadGame) # should always be available
self.registerCommand('flippetywick', self.devMode)
self.registerCommand('options', self.options)
self.registerCommand('colorTest', self.colorTest)
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)
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
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
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
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:
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])))
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.name for i in self.gameBase.player.inventory]))
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
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.')
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
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()
def gameMode(self):
"""Mode for in-game."""
if not self.__inGame:
self.registerCommand('map', self.showMap)
self.registerCommand('wait', self.gameBase.wait)
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)
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."""
args = ['"' + a + '"' for a in args]
self.gameBase.parseScript(' '.join(args) + '\n')
# 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 the lists of items again.
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)
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))
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
def playercmd(self, args):
self.handleCommand(args)
def update(self):
self.gameBase.gameEventLoop()
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])
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])
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/

309
src/gameshell/shell.py Normal file
View file

@ -0,0 +1,309 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# gameshell.py
#
# Copyright 2018 chees <chees@DESKTOP-0CA7MCF>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# Ver. 0.1.0033
import types as _types
import traceback as _tb
import math as _math
import re as _re
_CONV24TO8 = 6 / 256
class Shell(object):
def __init__(self):
self.__commands = {'exit': self.exitShell, 'batch': self.batch, 'alias': self.makeAlias, 'def': self.defineBatch}
self.__aliases = {}
self.__batches = {}
self.ps1 = '> '
self.ps2 = '. '
self.colorMode = 0 # bits of color depth. Supports: 0, 3, 4, 8, 24
self.prevColor = '\x1b[0m'
self.__exit = False
def __color24(self, r, g, b, fg = True):
if fg:
return '\x1b[38;2;{0};{1};{2}m'.format(r, g, b)
else:
return '\x1b[48;2;{0};{1};{2}m'.format(r, g, b)
def __color8(self, r, g, b, fg = True):
r = _math.floor(r * _CONV24TO8)
g = _math.floor(g * _CONV24TO8)
b = _math.floor(b * _CONV24TO8)
ret = 16 + b + 6 * g + 36 * r
if fg:
return '\x1b[38;5;{0}m'.format(ret)
else:
return '\x1b[48;5;{0}m'.format(ret)
def __color4(self, r, g, b, fg = True):
color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128)
r2, g2, b2 = r % 128, g % 128, b % 128
if r2 + g2 + b2 >= 192:
color += 60
if fg:
color += 30
else:
color += 40
return '\x1b[{0}m'.format(color)
def __color3(self, r, g, b, fg = True):
color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128)
if fg:
color += 30
else:
color += 40
return '\x1b[{0}m'.format(color)
def colorFromHex(self, color):
"""expects string formmatted like 3377DD"""
return int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16)
def color(self, r, g = 0, b = 0, fg = True, setPrevColor = True):
if isinstance(r, str):
r, g, b = self.colorFromHex(r)
ret = ''
if self.colorMode == 0: # no color
ret = ''
elif self.colorMode == 3:
ret = self.__color3(r, g, b, fg)
elif self.colorMode == 4:
ret = self.__color4(r, g, b, fg)
elif self.colorMode == 8:
ret = self.__color8(r, g, b, fg)
elif self.colorMode == 24:
ret = self.__color24(r, g, b, fg)
else:
ret = ''
if ret == self.prevColor:
return ''
if setPrevColor:
self.prevColor = ret
return ret
def setColor(self, r, g = 0, b = 0, fg = True):
"""Set the text color."""
print(color(r, g, b, fg), end = '')
return
def clearColor(self, setPrevColor = True):
ret = ''
if self.colorMode > 0:
ret = '\x1b[0m'
if ret == self.prevColor:
return ''
if setPrevColor:
self.prevColor = ret
return ret
def setClearColor(self):
print(self.clearColor())
return
def run(self):
"""The main game/shell loop"""
while not self.__exit:
print(self.ps1, end = '')
command = self.scanInput()
# we have to handle shell built-ins first (when we get some)
self.handleCommand(command)
self.update()
self.__exit = False
def man(self, args):
if (len(args)):
help(self.__commands[args[0]])
else:
print("Usage: help <commamd>\nCommands:\n\t{}".format('\n\t'.join(list(self.__commands.keys()))))
def registerCommand(self, commandName: str, command: _types.FunctionType):
"""command must be a function that takes one argument: a list of strings,
conventionally called args or argv"""
self.__commands[commandName] = command
def unRegisterCommand(self, commandName: str):
"""Remove a command. May be useful for inheriting classes and the like."""
del self.__commands[commandName]
def registerAlias(self, a: str, original: list):
"""makes 'a' an alias for original.
'a' must be one token, but original can be multiple."""
self.__aliases[a] = original
def unRegisterAlias(self, commandName: str):
"""Remove an alias. May be useful for inheriting classes and the like."""
del self.__aliases[commandName]
def registerBatch(self, name: str, commands: list):
self.__batches[name] = commands
def unRegisterBatch(self, commandName: str):
"""Remove a command. May be useful for inheriting classes and the like."""
del self.__batches[commandName]
def getAlias(self, a: str):
if a in self.__aliases:
return self.__aliases[a]
else:
return None
def getBatch(self, name: str):
if name in self.__batches:
return ['batch']
else:
return None
def scanLine(self, instr):
"""Take a line of text and turn it into an argument list"""
if instr == '':
return []
literalStr = list(instr)
inQuotes = False
ret = []
argStart = 0
c = 0
l = 0
while c < len(instr):
if inQuotes:
if instr[c] == '"':
if instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
inQuotes = False
del literalStr[l]
l -= 1
else:
if instr[c] == '"':
if l > 0 and instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
inQuotes = True
del literalStr[l]
l -= 1
elif instr[c] in ' \t\n':
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
argStart = l + 1
c += 1
l += 1
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
a = self.getAlias(ret[0])
if a:
ret = a + ret[1:]
b = self.getBatch(ret[0])
if b:
ret = b + ret
ret[0] = _re.sub(r'\A\$\((\w+)\)', r'\1', ret[0])
return ret
# Default functions
def exitShell(self, args):
"""The default exit command."""
self.__exit = True
def makeAlias(self, args):
"""Make an alias."""
self.registerAlias(args[0], args[1:])
def defineBatch(self, args):
"""Define a batch file."""
if len(args) < 1:
raise ValueError('def takes at least one argument')
ret = []
command = input(self.ps2)
while command != 'end':
ret.append(command)
command = input(self.ps2)
for i in args:
self.registerBatch(i, ret)
def batch(self, args):
"""Run commands in batch mode
$0 is the name of the batch
$1 - $n are individual arguments
$* is all the arguments except 0
$>0 - $>n is all arguments after the nth
$>=0 - $>=n is the nth and later arguments"""
script = self.__batches[args[0]]
var = _re.compile(r'\$((?:[>][=]?)?[0-9]+)')
u = False
for line in script:
if u:
self.update()
else:
u = True
newLine = line.replace(r'$*', ' '.join(args[1:]))
matches = var.finditer(newLine)
for m in matches:
n = m.group(1)
if n[0] == '>':
if n[1] == '=':
num = int(n[2:])
else:
num = int(n[1:])+1
newLine = newLine.replace(m.group(), ' '.join(args[num:]))
else:
newLine = newLine.replace(m.group(), args[int(n)])
newLine = _re.sub(r'\A({0})'.format(args[0]), r'$(\1)', newLine)
#print(newLine)
self.handleCommand(self.scanLine(newLine))
# Beyond this point are functions that are called within the main loop.
def scanInput(self):
"""Parses input. Override this for custom input parsing, or input source."""
return self.scanLine(input())
def handleCommand(self, command):
if len(command) == 0:
return
if command[0] in self.__commands:
try:
self.__commands[command[0]](command[1:])
except Exception as e:
_tb.print_exc()
print(e)
else:
self.handleUnknownCommand(command)
def handleUnknownCommand(self, command: list):
"""Handle commands that aren't registered. Override this if you want to do
something with those commands."""
print(f"Unknown command: {' '.join(command)}")
def update(self):
"""Runs at the end of each loop. Does nothing by default. Override this if
there is something you want the shell to do between every command."""
pass
def main(args):
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))

44
src/gameshell/tile.py Normal file
View file

@ -0,0 +1,44 @@
from tkinter import *
class Tile:
"""A representation of a tile on the display"""
shapes = {'circle' : 'o', 'cross' : 'x', 'triangle' : '^', 'none' : ' ',
'square' : '#', 'vertical' : '|', 'horizontal' : '-'}
def __init__(self, bgroundColor, fgroundColor = 'white', fgroundShape = ' '):
self.bgc = bgroundColor
self.fgc = fgroundColor
self.fgs = fgroundShape
def paint(self, display, x, y): #display being a canvas
if type(display) != Canvas:
raise TypeError('Display must be a tkinter.Canvas.')
else:
tag = '(' + str(int(x)) + ', ' + str(int(y)) + ')'
display.delete(tag) #delete the old tile before creating a new one.
if self.bgc != 'clear':
display.create_rectangle((x*32, y*32, x*32+32, y*32+32),
fill = self.bgc, width = 0, tags = (tag))
if self.fgs == Tile.shapes['triangle']:
display.create_polygon((x*32+15, y*32+2, x*32+2, y*32+30,
x*32+30, y*32+30, x*32+16, y*32+2),
fill = self.fgc, width = 0, tags = (tag))
elif self.fgs == Tile.shapes['circle']:
display.create_oval((x*32+2, y*32+2, x*32+30, y*32+30),
fill = self.fgc, width = 0, tags = (tag))
elif self.fgs == Tile.shapes['cross']:
display.create_line((x*32+2, y*32+2, x*32+30, y*32+30),
fill = self.fgc, width = 3, tags = (tag))
display.create_line((x*32+30, y*32+2, x*32+2, y*32+30),
fill = self.fgc, width = 3, tags = (tag))
elif self.fgs == Tile.shapes['square']:
display.create_rectangle((x*32+2, y*32+2, x*32+30, y*32+30),
fill = self.fgc, width = 0, tags = (tag))
elif self.fgs == Tile.shapes['vertical']:
display.create_line((x*32+16, y*32, x*32+16, y*32+32),
fill = self.fgc, width = 3, tags = (tag))
elif self.fgs == Tile.shapes['horizontal']:
display.create_line((x*32, y*32+16, x*32+32, y*32+16),
fill = self.fgc, width = 3, tags = (tag))
else: pass