# 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 . # # Copyright 2009, 2010, 2011, 2020 Jeremy Penner from engine import * import config from zope.interface import implements, Interface import weakref from util import * class Blinker(Token): def InitPersistent(self, owner, setter, rgVal = [True, False], interval = 500): Token.InitPersistent(self, owner) self.setter = setter self.rgVal = rgVal self.interval = interval def run(self): while True: for val in self.rgVal: self.setter(val) self.wait(self.interval) class StaticGraphic(Token): def InitPersistent(self, owner, ascr, pos, *rgtag): Token.InitPersistent(self, owner) self.pos = pos self.tag("drawable", *rgtag) self.ascr = ascr def draw(self, ascr, client): ascr.PutAscr(self.ascr, self.pos[0], self.pos[1]) class AnimatedGraphic(StaticGraphic): def InitPersistent(self, owner, rgascr, pos, interval, *rgtag): StaticGraphic.InitPersistent(self, owner, rgascr[0], pos, *rgtag) Blinker(self, self.setAscr, rgascr, interval) def setAscr(self, ascr): self.ascr = ascr class Terminal(Token): def InitPersistent(self, owner, client, w = config.W, h = config.H): Token.InitPersistent(self, owner, "drawable", "background") self.client = client self.x = 0 self.y = 0 self.w = w self.h = h self.fWrapHoriz = False self.fWrapVert = True self.stLine = None self.xLine = None self.yLine = None self.screen = [] self.fBlink = False self.colBg = ansi.BLUE self.chEcho = None for y in range(h): self.screen.append("") Blinker(self, self.setBlink) def setBlink(self, fBlink): self.fBlink = fBlink def getKey(self): return self.client.EvKey(self.game).receive(self) def getPrintableKey(self): while True: key = self.getKey() if ansi.FKeyPrintable(key): return key def deleteCh(self): stOld = self.screen[self.y] self.screen[self.y] = stOld[0:self.x] + stOld[self.x + 1:] def printSt(self, st): stOld = self.screen[self.y] stNew = stOld[0:self.x] + st + stOld[self.x+len(st):] self.screen[self.y] = stNew self.moveTo(self.x + len(st), self.y) def moveTo(self, x, y): self.x = x self.y = y while (self.x >= self.w): if (self.fWrapHoriz): self.y = self.y + 1 self.x = self.x - self.w else: self.x = self.w - 1 while (self.x < 0): self.y = self.y - 1 self.x = self.x + self.w if (self.y < 0): self.y = 0 while (self.fWrapVert and self.y >= self.h): self.screen = self.screen[1:] self.screen.append("") self.y = self.y - 1 self.screen[self.y] = self.screen[self.y].ljust(self.x) def type(self, st): self.client.beep() for iSt in range(len(st)): self.printSt(st[iSt:iSt + 1]) self.wait(35) def typeLn(self, st): self.type(st) self.newLine() def centerLn(self, st): self.typeLn(st.rjust((self.w / 2) + (len(st) / 2))) def getLine(self): self.xLine = self.x self.yLine = self.y self.stLine = "" fWrapHorizStart = self.fWrapHoriz self.fWrapHoriz = False while True: key = self.getKey() fBeep = True if (key == ansi.K_BACKSPACE and self.x > self.xLine): self.stLine = self.stLine[:self.x - self.xLine - 1] + self.stLine[self.x - self.xLine:] self.moveTo(self.x - 1, self.y) elif (key == ansi.K_LEFT and self.x > self.xLine): self.moveTo(self.x - 1, self.y) elif (key == ansi.K_RIGHT): self.moveTo(self.x + 1, self.y) if (self.x - self.xLine) > len(self.stLine): self.stLine = self.stLine + " " elif (ansi.FKeyPrintable(key)): self.stLine = self.stLine[:self.x - self.xLine] + key + self.stLine[(self.x + 1) - self.xLine:] self.moveTo(self.x + 1, self.y) elif key == ansi.K_RETURN or key == ansi.K_NEWLINE: break else: print "weird key", ansi.StrKey(key) fBeep = False if fBeep: self.client.beep() self.fWrapHoriz = fWrapHorizStart x,y = self.x, self.y self.moveTo(self.xLine, self.yLine) self.printSt(self.StLineDisp()) self.moveTo(x,y) stLine = self.stLine.strip() self.stLine = None self.xLine = None self.yLine = None return stLine def StLineDisp(self): return self.stLine if not self.chEcho else self.chEcho * len(self.stLine) def draw(self, ascr, client): if client == self.client: y = 1 for line in self.screen: ascr.PutSt("{0:{1}}".format(line, self.w), 1, y, ansi.WHITE | ansi.FBRIGHT, self.colBg) y = y + 1 if self.stLine: ascr.PutSt(self.StLineDisp(), self.xLine + 1, self.yLine + 1, ansi.WHITE | ansi.FBRIGHT, self.colBg) if (self.fBlink): ascr.PutAch(ansi.MkAch(" ", ansi.WHITE | ansi.FBRIGHT, ansi.WHITE), self.x + 1, self.y + 1) def spin(self, stSpin): self.moveTo(self.x - 1, self.y) self.printSt(stSpin) def newSpinner(self): return Blinker(self.game, self.spin, ["/","-","\\", "|"], 100) def newLine(self, count = 1, wait = 0): for i in range(count): self.moveTo(0, self.y + 1) self.beep() self.wait(wait) def beep(self): self.client.beep() def setColBg(self, colBg): self.colBg = colBg class LeaveJoinToken(Token): def run(self): evLeave = self.game.evSuspend if self.FOnActivate() else self.game.evLeave evJoin = self.game.evActivate if self.FOnActivate() else self.game.evJoin while True: Event.selectDg(self, (evLeave, self.OnLeave), (evJoin, self.OnJoin)) def FOnActivate(self): return False class AutoJoiner(LeaveJoinToken): def InitPersistent(self, owner, *rgclsTok): Token.InitPersistent(self, owner) self.rgclsTok = rgclsTok def InitTransient(self): Token.InitTransient(self) self.mpclient_rgtok = {} def OnLeave(self, client): for tok in self.mpclient_rgtok[client]: print "killing ", tok tok.die() def OnJoin(self, client): rgtok = [] for clsTok in self.rgclsTok: if FIter(clsTok): tok = clsTok[0](self.game, client, *clsTok[1:]) else: tok = clsTok(self.game, client) rgtok.append(tok) self.mpclient_rgtok[client] = rgtok class AliveWithPlayers(LeaveJoinToken): def InitPersistent(self, owner, *rgclsTok): Token.InitPersistent(self, owner) self.rgclsTok = rgclsTok def InitTransient(self): Token.InitTransient(self) self.rgclient = [] def FOnActivate(self): return True def OnLeave(self, client): self.rgclient.remove(client) if len(self.rgclient) == 0: for tok in [x for x in self.rgtokOwn]: tok.die() def OnJoin(self, client): self.rgclient.append(client) if len(self.rgclient) == 1: for clsTok in self.rgclsTok: if FIter(clsTok): tok = clsTok[0](self, clsTok[1:]) else: tok = clsTok(self) class AutoKiller(LeaveJoinToken): def InitTransient(self): Token.InitTransient(self) self.rgclient = [] def OnLeave(self, client): self.rgclient.remove(client) if len(self.rgclient) == 0: self.game.finish() def OnJoin(self, client): self.rgclient.append(client) #selection oracle class Selor(TPrs): NONE = 0 PRI = 1 SEC = 2 def __init__(self, mi, selorParent, rgmiSec, fPri): self.selorParent = selorParent self.mi = mi self.rgmiSec = rgmiSec self.fPri = fPri def MiParent(self): if self.selorParent != None: return self.selorParent.mi return None def KSel(self): if self.fPri: return self.PRI elif self.mi in self.rgmiSec: return self.SEC else: return self.NONE def SelorChild(self, miChild, fPri): return Selor(miChild, self, self.rgmiSec, self.fPri and fPri) def SelorDeselect(self): return Selor(self.mi, self.selorParent, self.rgmiSec, False) class Mi(TPrs): def InitPersistent(self): self.miParent = None self.fNotice = False self.rgmiNotice = [self] def Height(self, w): return 1 def StName(self): return "Unnamed" def StKey(self): return self.StName() def HandleKey(self, key): return False def FIgnoreParent(self): return False def DrawWithParent(self, selorParent, ascr, x, y, w, fPri): self.Draw(ascr, x, y, w, selorParent.SelorChild(self, fPri)) def ColBgDefault(self): return ansi.BLUE def ColBgSelected(self, fPri): return ansi.YELLOW if fPri else ansi.CYAN def ColBgNotice(self): return ansi.CYAN def ColBg(self, selor): if selor != None and selor.KSel() != Selor.NONE: if selor.KSel() == Selor.PRI: for mi in self.rgmiNotice: if mi != self and mi.fNotice: print "Notice cleared for ", mi.StName(), "by ", self.StName() mi.fNotice = False return self.ColBgSelected(True) else: return self.ColBgSelected(False) elif self.FNoticeFlagged(): return self.ColBgNotice() elif not self.FIgnoreParent() and selor != None and selor.MiParent() != None: return selor.MiParent().ColBg(selor.selorParent.SelorDeselect()) else: return self.ColBgDefault() def ColFg(self): return ansi.WHITE | ansi.FBRIGHT def FlagNotice(self): self.fNotice = True def ListenForNotice(self, mi): self.rgmiNotice.append(mi) def FNoticeFlagged(self): for mi in self.rgmiNotice: if mi.fNotice: return True return False def FSelectable(self): return True class MiInvisible(Mi): def Height(self, w): return 0 def Draw(self, *rgarg): pass def FSelectable(self): return False miInvisible = MiInvisible() class MiStatic(Mi): def InitPersistent(self, stText): Mi.InitPersistent(self) self.stText = stText def StName(self): return self.stText def FSelectable(self): return False def Height(self, w): return len(RgstWrapped(self.StName(), w, 0)) def Draw(self, ascr, x, y, w, selor): for st in RgstWrapped(self.StName(), w, 0): ascr.PutSt("{0:^{1}}".format(st, w), x, y, self.ColFg(), self.ColBg(selor)) y = y + 1 class MiDgText(MiStatic): def InitPersistent(self, dgText): MiStatic.InitPersistent(self, None) self.dgText = dgText def StName(self): return self.dgText() class MiButton(MiStatic): def InitPersistent(self, stName, dgCmd, value = None): MiStatic.InitPersistent(self, stName) self.dgCmd = dgCmd self.value = value def StName(self): if self.stText == None and self.value != None: return self.value.StName() return self.stText def Exec(self): return self.dgCmd() def FSelectable(self): return True def HandleKey(self, key): if ansi.FEnter(key) or key == ' ': return self.Exec() return False def Value(self): return self.value class MiButtonConfirm(MiButton): def InitPersistent(self, stName, stConfirm, dgCmd): super(MiButtonConfirm, self).InitPersistent(stName, dgCmd) self.stConfirm = stConfirm self.fConfirming = False def StName(self): if self.fConfirming: return self.stConfirm return super(MiButtonConfirm, self).StName() def HandleKey(self, key): if ansi.FEnter(key) or key == ' ': if self.fConfirming: self.Exec() else: self.fConfirming = True return True else: self.fConfirming = False return False def ColBg(self, selor): if self.fConfirming: return ansi.RED return super(MiButtonConfirm, self).ColBg(selor) class MiToggle(MiButton): def InitPersistent(self, stName, fInitial = False): MiButton.InitPersistent(self, stName, None, fInitial) def Exec(self): self.value = not self.value return True def StName(self): stPre = "[X] " if self.value else "[ ] " return stPre + self.stText def StKey(self): return self.stText class MiList(Mi): def InitPersistent(self, rgval, ival = 0): Mi.InitPersistent(self) self.rgval = rgval self.ival = ival def Value(self): return self.rgval[self.ival] def Inc(self): if self.ival < len(self.rgval) - 1: self.ival = self.ival + 1 def Dec(self): if self.ival > 0: self.ival = self.ival - 1 def HandleKey(self, key): if key == ansi.K_LEFT: self.Dec() elif key == ansi.K_RIGHT: self.Inc() else: return False return True def Draw(self, ascr, x, y, w, selor): xText = w / 4 xVal = w / 2 ascr.PutSt(self.StName() + ":", xText, y, self.ColFg(), self.ColBg(selor)) self.DrawValue(ascr, xVal, y, selor.SelorDeselect()) class MiTypein(Mi): def InitPersistent(self, dgEnter, stName = "Typein", pctW = 100): Mi.InitPersistent(self) self.stedit = Stedit(dgEnter) self.stName = stName self.pctW = pctW assert pctW > 0 and pctW <= 100 def StName(self): return self.stName def Value(self): return self.stedit.GetValue() def SetValue(self, st): self.stedit.SetValue(st) def HandleKey(self, key): return self.stedit.HandleKey(key) def ColBgDefault(self): return ansi.BLACK def ColBgSelected(self, fPri): return ansi.WHITE if fPri else ansi.BLUE def ColFg(self): return ansi.WHITE def FIgnoreParent(self): return True def Height(self, wT): return len(RgstWrapped(self.GetStToWrap(), self.GetW(wT))) def GetW(self, wT): return int(wT * (self.pctW / 100.0)) def GetStToWrap(self): return self.stedit.StForSize() def Draw(self, ascr, xT, y, wT, selor): w = self.GetW(wT) x = xT + ((wT - w) / 2) if selor.KSel() != Selor.NONE: stToWrap = self.GetStToWrap() else: stToWrap = self.stedit.GetValue() for snippet in RgSnippetWrapped(stToWrap, w): ascr.PutSt("{0:{1}}".format(snippet.st, w), x, y, self.ColFg(), self.ColBg(selor.SelorDeselect())) if selor.KSel() != Selor.NONE: self.stedit.DrawCursor(ascr, x, y, self.ColFg(), self.ColBg(selor), snippet) y = y + 1 class MiTab(MiStatic): def InitPersistent(self, stText, mi): MiStatic.InitPersistent(self, stText) self.mi = mi self.ListenForNotice(mi) def Value(self): return self.mi def FSelectable(self): return True @staticmethod def MiTabbed(*rgstTab_mi): miTabRow = MiMenuHoriz([MiTab(*stTab_mi) for stTab_mi in rgstTab_mi]) return MiMenu([miTabRow, MiTabBody(miTabRow)]) class MiTabBody(Mi): def InitPersistent(self, miMenu): Mi.InitPersistent(self) self.miMenu = miMenu def Mi(self): return self.miMenu.Value() def HandleKey(self, key): return self.Mi().HandleKey(key) def Draw(self, ascr, x, y, w, selor): self.Mi().Draw(ascr, x, y, w, selor) def Height(self,w): return self.Mi().Height(w) class MiValue(Mi): def InitPersistent(self, mi): Mi.InitPersistent(self) self.mi = mi def StName(self): return self.mi.StName() def Value(self): return self.mi.Value() def Height(self,w): return self.mi.Height(w) def Draw(self, ascr, x, y, w, selor): self.mi.DrawValue(ascr, x, y, selor.SelorChild(self.mi, False)) class Selection(TPrs): def InitPersistent(self, rgo, o = None, io = 0): self.rgo = rgo self.o = o self.io = -1 if o == None else io self.Validate() def Invalidate(self): self.o = None self.io = -1 def Validate(self): # aw hell, selection only handles inserts, not deletes :( if self.o == None or self.io < 0: self.Invalidate() elif len(self.rgo) <= self.io or self.rgo[self.io] != self.o: try: self.io = self.rgo.index(self.o) except Exception: self.Invalidate() if self.o == None and not self.FEmpty(): self.io = IoInc(self.rgo, self.io, self.FSelectableI) self.o = self.rgo[self.io] def GetSelected(self): self.Validate() return self.o def SetSelected(self, o): self.o = o self.Validate() def ITopScroll(self, h, dgHeight): # requirements: # selection must be visible # if possible, selection should be centered if self.io < 0: return 0 iotop = self.io iobot = self.io hCurr = dgHeight(self.o) yobot = hCurr fGrowUp = False while hCurr < h: fDoneUp = iotop == 0 fDoneDown = iobot == len(self.rgo) - 1 if (fGrowUp or fDoneDown) and not fDoneUp: ho = dgHeight(self.rgo[iotop - 1]) if (yobot + ho >= h): # handle case where thing above selected thing is huge and pushes the selection off the screen break iotop = iotop - 1 yobot = yobot + ho elif not fDoneDown: ho = dgHeight(self.rgo[iobot + 1]) iobot = iobot + 1 else: break hCurr = hCurr + ho fGrowUp = not fGrowUp return iotop def Inc(self): return self.ChangeI(IoInc) def Dec(self): return self.ChangeI(IoDec) def FEmpty(self): for o in self.rgo: if self.FSelectableI(o): return False return True def ChangeI(self, fnChange): self.Validate() if self.o != None: ioT = self.io self.io = fnChange(self.rgo, self.io, self.FSelectableI) self.o = self.rgo[self.io] return ioT != self.io def FSelectableI(self, o): return o != None and o.FSelectable() class MiMenuI(Mi): def InitPersistent(self, rgmi): Mi.InitPersistent(self) self.selMi = Selection(rgmi) self.mpstName_mi = {} for mi in rgmi: if mi != None: self.mpstName_mi[mi.StKey().lower()] = mi def Value(self, stKey = None): if stKey == None: return self.selMi.GetSelected().Value() return self.mpstName_mi[stKey.lower()].Value() def MiByName(self, stKey): return self.mpstName_mi[stKey.lower()] def HeightMi(self, mi, w): if mi == None: return 1 return mi.Height(w) def ColBgSelected(self, fPri): return self.ColBgDefault() class MiMenu(MiMenuI): def InitPersistent(self, rgmi, hMax = -1): MiMenuI.InitPersistent(self, rgmi) self.hMax = hMax def HandleKey(self, key): mi = self.selMi.GetSelected() if mi == None or not mi.HandleKey(key): if key == ansi.K_DOWN: return self.selMi.Inc() elif key == ansi.K_UP: return self.selMi.Dec() return False return True def HMax(self, ascr, y): if self.hMax < 0: return (ascr.H() - y) + 1 return self.hMax def Height(self, w): return sum([self.HeightMi(mi,w) for mi in self.selMi.rgo]) def Draw(self, ascr, x, y, w, selor): ascr.Fill(ansi.MkAch(' ', self.ColFg(), self.ColBg(selor)), w, self.Height(w), x, y) miSelected = self.selMi.GetSelected() h = 0 hMax = self.HMax(ascr, y) itop = self.selMi.ITopScroll(hMax, lambda mi: self.HeightMi(mi, w)) for imi, mi in enumerate(self.selMi.rgo): if imi < itop: continue if mi != None: mi.DrawWithParent(selor, ascr, x, y + h, w, miSelected == mi) h = h + self.HeightMi(mi, w) if h >= hMax: break class MiMenuHoriz(MiMenuI): def InitPersistent(self, rgmi, rgsize = None): MiMenuI.InitPersistent(self, rgmi) if rgsize == None: rgsize = [1 for mi in rgmi] self.rgsize = rgsize self.sizeMax = float(sum(rgsize)) def Width(self, imi, w): return int((self.rgsize[imi] / self.sizeMax) * w) def HandleKey(self, key): mi = self.selMi.GetSelected() if mi == None or not mi.HandleKey(key): if key == ansi.K_RIGHT: return self.selMi.Inc() elif key == ansi.K_LEFT: return self.selMi.Dec() return False return True def Height(self, w): return max([self.HeightMi(mi, w) for mi in self.selMi.rgo]) def Draw(self, ascr, x, y, w, selor): ascr.Fill(ansi.MkAch(' ', self.ColFg(), self.ColBg(selor)), w, self.Height(w), x, y) miSelected = self.selMi.GetSelected() for imi, mi in enumerate(self.selMi.rgo): wT = self.Width(imi, w) if mi != None: mi.DrawWithParent(selor, ascr, x, y, wT, miSelected == mi) x = x + wT class ProjectedValue(TPrs): def InitPersistent(self, value, dgStName): self.value = value self.dgStName = dgStName def StName(self): return self.dgStName(self.value) def Value(self): return self.value def DgProjectMiButton(dgExec, dgStName = None): def DgProject(value): return MiButton(None, lambda: dgExec(value), ProjectedValue(value, dgStName) if dgStName else value) return DgProject class RgmiProjected(TPrs): def InitPersistent(self, rgvalue, dgProject): self.rgvalue = rgvalue self.dgProject = dgProject def InitTransient(self): self.mpvalue_wrmi = weakref.WeakKeyDictionary() def __getitem__(self, i): return self.MiFromValue(self.rgvalue[i]) def MiFromValue(self, value): if value in self.mpvalue_wrmi: mi = self.mpvalue_wrmi[value]() if mi != None: return mi mi = self.dgProject(value) mi._valueProjected = value self.mpvalue_wrmi[value] = weakref.ref(mi) # this probably gc's much more than is necessary ... :/ return mi def __len__(self): return len(self.rgvalue) def index(self, mi): return self.rgvalue.index(mi._valueProjected) class OvStatic(TokenClient): TOP = 1 MIDDLE = 2 BOTTOM = 3 FILL = 4 def InitPersistent(self, owner, client, miMenu, kplace, *rgtag): TokenClient.InitPersistent(self, owner, client, "drawable", "menu", *rgtag) self.miMenu = miMenu self.kplace = kplace self.mpst_miSel = {} def SetSelection(self, stSel, mi): self.mpst_miSel[stSel] = mi def GetSelection(self, stSel): try: return self.mpst_miSel[stSel] except KeyError: return None def draw(self, ascr, client): self.drawI(ascr, client, False) def drawI(self, ascr, client, fSelected): if client == self.client: selor = Selor(self.miMenu, None, self.mpst_miSel.values(), fSelected) if self.kplace & self.FILL: h = ascr.H() y = 1 dh = h - self.miMenu.Height(ascr.W()) if dh > 0: ascr.Fill(ansi.MkAch(' ', self.miMenu.ColFg(), self.miMenu.ColBg(selor)), ascr.W(), dh, 1, ascr.H() - dh + 1) else: h = self.miMenu.Height(ascr.W()) if self.kplace == self.TOP: y = 1 elif self.kplace == self.MIDDLE: y = (ascr.H() / 2) - (h / 2) else: # BOTTOM y = ascr.H() - h + 1 self.miMenu.Draw(ascr, 1, y, ascr.W(), selor) def Value(self, stName = None): return self.miMenu.Value(stName) class OvMenu(OvStatic): def run(self): while True: self.miMenu.HandleKey(self.EvKey().receive(self)) def draw(self, ascr, client): self.drawI(ascr, client, True) class OvPopup(OvStatic): def InitPersistent(self, owner, client, miMenu): OvStatic.InitPersistent(self, owner, client, miMenu, self.MIDDLE, "drawable", "menu") self.evShow = Event(self.game) self.evDone = Event(self.game) self.fAwake = False def run(self): while True: self.evShow.receive(self) self.fAwake = True while self.fAwake: key = self.EvKey().receive(self) if not self.miMenu.HandleKey(key): if key == ansi.K_TAB or key == ansi.K_DEL or key == ' ' or ansi.FEnter(key): self.fAwake = False self.evDone.fire() def draw(self, ascr, client): if self.fAwake: self.drawI(ascr, client, True) class MsgScroller(Token): def InitPersistent(self, owner, colFg=(ansi.CYAN | ansi.FBRIGHT), colBg=ansi.BLACK, y=config.H): Token.InitPersistent(self, owner, "drawable", "msgscroller", "overlay") self.colFg = colFg self.colBg = colBg self.rgmsg = [] self.msg = None self.evPost = Event(self.game) self.y = y def postI(self, msg): self.rgmsg.append(msg) def run(self): while True: self.postI(self.evPost.receive(self)) with self.evPost.oob(self, self.postI): while len(self.rgmsg) > 0: self.msg = self.rgmsg[0] self.rgmsg = self.rgmsg[1:] self.x = config.W while self.x > -len(self.msg): self.x = self.x - 1 self.wait(30) self.msg = None def draw(self, ascr, client): if self.msg: ascr.PutSt(self.msg, self.x, self.y, self.colFg, self.colBg)