marmots/engine.py
Jeremy Penner 4e720f0d67 Refactor / simplify scripting syntax classes
* Introduce Stype, a generalized object for producing synts by text matching
* consistently use stype to determine what can be inserted in a given place
* rewrite SyntDesc to more extensively use Stypes
* Introduce SyntHole, a generalized anyblob that can be put anywhere and evals
  to failure
* Remove all data types but flags, introduce bots
* Remove math
* Remove compiler / VM
* Remove unused methods
2020-07-05 17:17:46 -04:00

574 lines
16 KiB
Python

# This file is part of MarMOTS.
#
# MarMOTS is free software: you can redistribute it and/or modify it under the terms of the GNU Affero
# General Public License as published by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# MarMOTS 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 Affero General
# Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with MarMOTS. If not,
# see <https://www.gnu.org/licenses/>.
#
# Copyright 2009, 2010, 2011, 2020 Jeremy Penner
import stackless
import greenlet
import random
import logging
import pickle
import time
import ansi_cython as ansi
import auth
import config
import telnet
import traceback
from twisted.internet import reactor
from tpers import TPrs, TPLS, Version
import tpers
# TPrs - persistent base class; includes migration facilities and the ability to selectively opt-out of
# being persisted. Most long-lived objects should probably be derived from TPrs.
# Ownable - has a parent ownable; die() will be called when its parent dies.
# Taggable - ownable, and also has tags. Assumes an eventual Game ancestor where the tag index is stored.
# Token - taggable, and also has a tasklet that runs for as long as it's alive.
class Event(TPrs):
def InitPersistent(self, game):
self.game = game
def InitTransient(self):
self.rgtk = TPLS()
self.queueValue = []
def oob(self, tok, dgoob):
return Oob(self, tok, dgoob)
def receive(self, tok):
chNotify = tok.chEv
self.rgtk.append(tok)
while True:
ev, value = chNotify.receive()
if ev == self:
return value
ev.rgtk.append(tok)
if ev in tok.mpevDgoob and tok.mpevDgoob[ev](value):
raise Event.OobBreakException()
@classmethod
def select(cls, tok, *rgev):
for ev in rgev:
ev.rgtk.append(tok)
chNotify = tok.chEv
fOob = False
while True:
evRcv, value = chNotify.receive()
if evRcv in tok.mpevDgoob:
if tok.mpevDgoob[ev](value):
fOob = True
break
else:
break
for ev in rgev:
if ev != evRcv:
ev.rgtk.remove(tok)
if fOob:
raise Event.OobBreakException()
return evRcv, value
@classmethod
def selectDg(cls, tok, *rgev_dg):
rgev, rgdg = zip(*rgev_dg)
ev, value = cls.select(tok, *rgev)
return rgdg[rgev.index(ev)](value)
def fire(self, value = None):
self.queueValue.append(value)
self.game.queueEv.append(self)
self.game.ensureRun()
def fireI(self):
rgtkOld = self.rgtk
value = self.queueValue[0]
self.queueValue = self.queueValue[1:]
self.rgtk = TPLS()
for tk in rgtkOld:
if tk.fRunning:
tk.chEv.send((self,value))
stackless.run()
class Oob(object):
def __init__(self, ev, tok, dgoob):
self.ev = ev
self.tok = tok
self.dgoob = dgoob
def __enter__(self):
self.ev.rgtk.append(self.tok)
self.tok.mpevDgoob[self.ev] = self.dgoob
def __exit__(self, etype, eval, tb):
if etype != greenlet.GreenletExit:
try:
self.tok.mpevDgoob.pop(self.ev)
self.ev.rgtk.remove(self.tok)
if etype == OobBreakException:
return True
except Exception:
print "exception exiting oob:"
traceback.print_exc()
print "oob:", etype, eval
traceback.print_tb(tb)
class OobBreakException(Exception):
pass
class Runner(TPrs):
def InitPersistent(self):
self.mpuser_cld = {}
print "New Runner created"
def InitTransient(self):
self.rgclient = []
def GetCld(self, user):
cld = self.mpuser_cld.get(user)
if cld == None:
cld = Cld(user)
self.mpuser_cld[user] = cld
return cld
def RunGameI(self, client):
client.runner = self
self.rgclient.append(client)
def OnDisconnect():
self.rgclient.remove(client)
client.addDgQuit(OnDisconnect)
return self.RunGame(client)
def RunServer(self):
for client in self.rgclient:
client.quit(True)
telnet.RunServer(self.RunGameI)
def Run(clsRunner, fn):
runner = tpers.Odb.Load(fn) or clsRunner()
def ExecCmd(stCmd):
if stCmd == "save":
tpers.Odb.Save(runner, fn)
return "Saved to " + fn
return "Unrecognized command: '" + stCmd + "'"
telnet.RunCmdServer(ExecCmd)
runner.RunServer()
class Ownable(TPrs):
def InitPersistent(self, owner = None):
self.owner = owner
self.rgtokOwn = TPLS()
if owner != None:
owner.rgtokOwn.append(self)
def FPersist(self):
if self.owner != None and not self.owner.FPersist():
return False
return TPrs.FPersist(self)
def TransferToOwner(self, ownerNew):
self.owner.rgtokOwn.remove(self)
ownerNew.rgtokOwn.append(self)
self.owner = ownerNew
def die(self):
if self.owner != None:
self.owner.rgtokOwn.remove(self)
self.owner = None
for tok in [x for x in self.rgtokOwn]:
tok.die()
self.DontPersist()
@Version(2)
class Game(Ownable):
def InitPersistent(self):
Ownable.InitPersistent(self)
self.fQuit = False
self.running = False
self.mptagTokenset = {}
self.rgdgdie = []
self.evJoin = Event(self)
self.evLeave = Event(self)
self.mpuser_cldg = {}
self.CreateRgtok(self.GetRgclsTokPers(), True)
def InitTransient(self):
self.MakeTransient("fScheduled", False)
self.rgclient = []
self.rgproc = [PTimer(), PEvent(), PDraw(*self.GetRgtagDraw())]
for proc in self.rgproc:
proc.setGame(self)
# don't recreate tokens when game has died (shouldn't even be persisting)
if not self.FDead():
self.CreateRgtok(self.GetRgclsTokTrans(), False)
else:
print "Dead", self.__class__, "has persisted"
def FDead(self):
try:
rc = self.rc
return True
except:
return False
def GetCldg(self, user):
cldg = self.mpuser_cldg.get(user)
if cldg == None:
cldg = Cldg()
self.mpuser_cldg[user] = cldg
return cldg
def GetRgclsTokPers(self):
return []
def GetRgclsTokTrans(self):
return []
def GetRgtagDraw(self):
return ["background", "solid", "overlay"]
def CreateRgtok(self, rgclsTok, fPersist):
for clsTok in rgclsTok:
if FIter(clsTok):
tok = clsTok[0](self, *clsTok[1:])
else:
tok = clsTok(self)
if not fPersist:
tok.DontPersist()
def tagToken(self, token, tag):
if not (tag in self.mptagTokenset):
self.mptagTokenset[tag] = set([token])
else:
self.mptagTokenset[tag].add(token)
def untagToken(self, token, tag):
if tag in self.mptagTokenset:
self.mptagTokenset[tag].discard(token)
def rgtoken(self, tag, rgpriority = []):
if not (tag in self.mptagTokenset):
return []
rgtoken = []
tokenSet = self.mptagTokenset[tag].copy()
for priority in rgpriority:
if (priority in self.mptagTokenset):
ptokenSet = self.mptagTokenset[priority] & tokenSet
for token in ptokenSet:
rgtoken.append(token)
tokenSet -= ptokenSet
for token in tokenSet:
rgtoken.append(token)
return rgtoken
def quit(self):
self.running = False
self.fQuit = True
def finish(self, rc = None):
self.running = False
self.rc = rc
class QuitException(Exception):
pass
class LoadException(Exception):
pass
def die(self):
for dgdie in self.rgdgdie:
dgdie()
Ownable.die(self)
def runStep(self):
try:
if self.running:
for proc in self.rgproc:
proc.process()
if not self.running:
self.die()
for client in self.rgclient:
client.leaveGame(self.rc)
for client in self.rgclient:
client.postRunStep()
finally:
self.fScheduled = False
return self.running
def joinGame(self,client):
self.rgclient.append(client)
self.evJoin.fire(client)
def leaveGame(self, client):
self.evLeave.fire(client)
def leaveGameI(self, client):
self.rgclient.remove(client)
def ensureRun(self):
self.running = True
if not self.fScheduled:
self.fScheduled = True
reactor.callLater(0, self.runStep)
def UpgradeFrom(self, versionOld):
if versionOld < 2:
self.mpuser_cldg = {}
# Client data
@Version(2)
class Cld(TPrs):
def InitPersistent(self, user):
self.user = user
def UpgradeFrom(self, versionOld):
if versionOld < 2:
del self.mpgame_cldg
#per-game client data
class Cldg(TPrs):
def FPersist(self):
if len(self._persistent) == 0:
return False
return TPrs.FPersist(self)
class Client(TPrs):
def InitTransient(self):
self.chRun = stackless.channel()
self.gameCurr = None
self.chCont = None
self.fRunning = False
self.dgJoinGame = None
self.rgdgQuit = []
self.fQuit = False
self.cld = None
self.runner = None
def FPersist(self):
return False
def go(self, fnRun, protocol):
self.protocol = protocol
self.tskRun = stackless.tasklet(fnRun)
self.tskRun(self)
if self._runTskRun():
self.gameCurr.ensureRun()
def joinGame(self, game):
assert not self.fRunning, "already running"
chCont = stackless.channel()
self.chRun.send((game, chCont))
return chCont.receive()
def leaveGame(self, rc = None):
self.fRunning = False
self.gameCurr.leaveGame(self)
self.rc = rc
def quit(self):
self.leaveGame(None)
self.fQuit = True
def _runTskRun(self):
"run self.tskRun after it has been scheduled, and await results"
stackless.run()
if not self.tskRun.scheduled:
return False
self.gameCurr, self.chCont = self.chRun.receive()
self.gameCurr.joinGame(self)
self.fRunning = True
if self.dgJoinGame:
self.dgJoinGame(self, self.gameCurr)
return True
def postRunStep(self):
if not self.fRunning:
self.gameCurr.leaveGameI(self)
if not self.fQuit:
self.chCont.send(self.rc)
self.fQuit = not self._runTskRun()
if self.fQuit:
for dgQuit in self.rgdgQuit:
dgQuit()
return self.fQuit
def beep(self):
pass
def login(self, user, passwd):
authr = getattr(auth, config.AUTH + "Auth")()
if (authr.FAuthorized(user, passwd)):
self.cld = self.runner.GetCld(user)
return True
return False
def addDgQuit(self, dgquit):
self.rgdgQuit.append(dgquit)
def removeDgQuit(self, dgquit):
self.rgdgQuit.remove(dgquit)
def Cldg(self, game = None):
if game == None:
game = self.gameCurr
return game.GetCldg(self.cld.user)
class Processor(TPrs):
def setGame(self, game):
self.game = game
def process(self):
pass
class PTimer(Processor):
def msNow(self):
return int(time.time() * 1000)
def setGame(self, game):
self.game = game
self.fReset = False
game.evTick = Event(game)
game.ms = 0
game.msDelta = 0
self.dms = -self.msNow()
def __setstate__(self, dict):
Processor.__setstate__(self, dict)
self.fReset = True
def process(self):
if self.fReset:
# keep game.ms constant
self.dms = self.game.ms - self.msNow()
self.fReset = False
msNew = self.msNow() + self.dms
self.game.msDelta = msNew - self.game.ms
self.game.ms = msNew
self.game.evTick.fire(self.game.msDelta)
class PEvent(Processor):
def setGame(self, game):
self.game = game
game.queueEv = []
game.rgdgdie.append(self.die)
def die(self):
for ev in self.game.queueEv:
for tk in ev.rgtk:
tk.die()
ev.stkValue = []
self.game.queueEv = []
def process(self):
while self.game.queueEv:
self.game.queueEv[0].fireI()
self.game.queueEv.pop(0)
class PDraw(Processor):
def InitPersistent(self, *rgprio):
self.rgprio = rgprio
self.msDrawAgain = -1
self.fTimerSet = False
def process(self):
if self.game.ms >= self.msDrawAgain:
for client in self.game.rgclient:
ascr = ansi.Ascr()
for tok in self.game.rgtoken("drawable", self.rgprio):
try:
tok.draw(ascr, client)
except Exception:
print "error drawing"
traceback.print_exc()
client.protocol.Draw(ascr)
self.msDrawAgain = self.game.ms + 120
self.fTimerSet = False
elif not self.fTimerSet:
reactor.callLater((self.msDrawAgain - self.game.ms) / 1000.0, self.game.ensureRun)
self.fTimerSet = True
class Taggable(Ownable):
def InitPersistent(self, owner, *rgtag):
Ownable.InitPersistent(self, owner)
game = owner
while not isinstance(game, Game):
game = game.owner
self.game = game
self.tags = set()
self.tag(*rgtag)
def tag(self, *rgtag):
self.tags |= set(rgtag)
for tag in rgtag:
self.game.tagToken(self, tag)
def untag(self, *rgtag):
self.tags -= set(rgtag)
for tag in rgtag:
self.game.untagToken(self, tag)
def die(self):
for tag in self.tags.copy():
self.untag(tag)
Ownable.die(self)
class Token(Taggable):
def InitPersistent(self, owner, *rgtag):
Taggable.InitPersistent(self, owner, *rgtag)
self.msGamePreWait = 0
self.ms = 0
self.fRunning = True
self.rgtokOwn = TPLS()
def InitTransient(self):
Taggable.InitTransient(self)
self.chEv = stackless.channel()
self.tasklet = stackless.tasklet(self.runI)
self.mpevDgoob = {}
self._fWriteToPersistent = True
self.tasklet()
def __enter__(self):
return self
def __exit__(self, etype, eval, traceback):
self.die()
def wait(self, msDelta):
# have we waited once already this tick? If not, reset
if (self.msGamePreWait != self.game.ms):
self.ms = self.game.ms
msEnd = self.ms + msDelta
delay = (msEnd - self.game.ms) / 1000.0
if delay > 0:
reactor.callLater(delay, self.game.ensureRun)
while (self.game.ms < msEnd):
self.game.evTick.receive(self)
self.ms = msEnd
self.msGamePreWait = self.game.ms
def die(self):
self.fRunning = False
Taggable.die(self)
self.tasklet.kill() # the token may be killing itself; make sure it's properly marked as dead before killing the tasklet
def run(self):
pass
def runI(self):
try:
self.run()
#except greenlet.GreenletExit:
# raise
except Exception:
print "token script crashed:"
traceback.print_exc()
def FIter(l):
try:
iter(l)
return True
except Exception:
return False
class TokenClient(Token):
def InitPersistent(self, owner, client, *rgtag):
Token.InitPersistent(self, owner, *rgtag)
self.client = client
def FPersist(self):
return False