### Kliffy - Klikable Interactive Fiction For You! (c)2010-2011 Jeremy Penner ### #lazy enumeration class Eni constructor: (@val, @fStarted) -> Value: () -> @val EniNext: () -> if not @fStarted then new Eni(@val, true) EniEmpty = { EniNext: (-> null) } class EniArray constructor: (@rg, @i) -> if not @i? then @i = -1 Value: -> @rg[@i] EniNext: -> if @i + 1 < @rg.length then new EniArray(@rg, @i + 1) class EniConcat constructor: (@eni1, @eni2) -> Value: -> @eni1.Value() EniNext: (rgarg...) -> eni1New = @eni1.EniNext(rgarg...) if eni1New? new EniConcat(eni1New, @eni2) else @eni2.EniNext(rgarg...) class EniThunk constructor: (@thunk) -> # it is always illegal to call Value on this eni, as eninext will never return an eniThunk EniNext: (rgarg...) -> # memoize generated eni if not @eni? @eni = @thunk(rgarg...) @eni.EniNext() class EniCons constructor: (@value, @thunk, @fStarted) -> Value: -> @value EniNext: (rgarg...) -> if not @fStarted if not @eniNext? @eniNext = new EniCons(@value, @thunk, true) @eniNext else if not @eni? @eni = @thunk(this, rgarg...) @eni.EniNext() class EniMap constructor: (@eni, @dg) -> Value: -> @dg(@eni.Value()) EniNext: (rgarg...) -> eniNext = @eni.EniNext() if eniNext? then new EniMap(eniNext, @dg, rgarg...) class EniFilter constructor: (@eni, @dgFilter) -> Value: -> @eni.Value() EniNext: (rgarg...) -> eni = @eni.EniNext(rgarg...) while eni? and not @dgFilter(eni.Value(), eni, rgarg...) eni = eni.EniNext() eni ArrayFromEni = (eni) -> rg = [] while (eni = eni.EniNext())? rg.push(eni.Value()) rg # world state -- holds the state of the world after every NewWst = (obj, gst) -> Ctor = (@gst) -> @WstNext = (nev) -> CtorNext = (wstOld) -> @nev = nev @wstPrev = wstOld this CtorNext.prototype = this new CtorNext(this) @WstPrev = () -> @wstPrev this Ctor.prototype = obj new Ctor(gst) class Respo constructor: (@enieniNev, @eniNev) -> if @enieniNev.eni.i < 0 and @eniNev? @eniNev = @eniNev @FromEnieniNev: (enieniNev) -> new Respo(enieniNev) Nev: () -> @eniNev.Value() Next: (nevgen) -> enieniNev = @enieniNev eniNev = @eniNev loop if not eniNev? enieniNev = enieniNev.EniNext() if enieniNev? eniNev = enieniNev.Value() else return null eniNev = eniNev.EniNext(nevgen, new Respo(enieniNev, eniNev)) if eniNev? then return new Respo(enieniNev, eniNev) null ReplaceEniNev: (eniNevNew) -> new Respo(@enieniNev, eniNevNew) class Nevgen constructor: (@gst, @wst, @eniWstResponding, @respo) -> @EniWst: (gst, wstInit) -> nevgen = new Nevgen(gst, wstInit, null, null) new EniCons(wstInit, nevgen.Thunk()) Thunk: () -> (eniPrev) => nevgen = this if not @eniWstResponding? nevgen = @RespondTo(@gst, @wst, eniPrev) nevgen = nevgen.Next() if nevgen? new EniCons(nevgen.wst, nevgen.Thunk()) else EniEmpty Next: () -> nevgen = this while nevgen? and nevgen.wst == @wst nevgen = nevgen.NextI() nevgen NextI: () -> if @wst.nev.FEndsSection() return null if @respo? respoNext = @respo.Next(this) wst = @wst if respoNext? and respoNext.Nev().FCanRun(@gst, wst) wst = @gst.RunNev(wst, respoNext.Nev()) new Nevgen(@gst, wst, @eniWstResponding, respoNext) else if @wst != @eniWstResponding.Value() @RespondTo(@gst, @wst, @eniWstResponding.EniNext()) RespondTo: (gst, wst, eniWstResponding) -> new Nevgen(gst, wst, eniWstResponding, gst.story.RespoResponse(eniWstResponding.Value().nev, wst)) # game state -- holds all the information needed to display the current state of the game in our browser class Gst constructor: (@story, @jDiv) -> @rgwstInit = [@story.WstInit(this)] @rgsection = [@story.SectionByName("start")] @rgwst = null @igenID = 0 GoBackOneSection: () -> isectionRemove = @rgsection.length - 1 @rgsection.splice(isectionRemove, 1) @rgwstInit.splice(isectionRemove, 1) @Display() PushSection: (section, wst) -> @rgsection.push(section) @rgwstInit.push(wst) @Display() SectionCurrent: () -> @rgsection[@rgsection.length - 1] Display: () -> @mpstId_dgOnClick = [] @ClearMenu(true) @jDiv.empty() @jDiv.click(() => @ClearMenu()) @jDiv.append("

#{@story.jStory.attr('title')}

#{@story.jStory.attr('author')}

") dgEnter = () -> $(this).addClass('hover') dgLeave = () -> $(this).removeClass('hover') # show "previous section" link if @rgsection.length > 1 @jDiv.append("
#{@Link("Previous section", () => @GoBackOneSection())}
") # show all nevs in section @rgwst = null @rgwst = @RgwstRun() for wst in @rgwst jDivNev = $("
") jDivNev.append(wst.nev.StHtmlDisplay(this, wst)) @jDiv.append(jDivNev) jDivNev.hover(dgEnter, dgLeave) @wstLast = wst # show "next section" link stHtmlNav = @wstLast.nev.StHtmlNextSection(this, @wstLast) if stHtmlNav? @jDiv.append("
#{stHtmlNav}
") for stId_dgOnClick in @mpstId_dgOnClick @jDiv.find("##{stId_dgOnClick[0]}").click(stId_dgOnClick[1]) # only valid to call after the story has been run FWasRun: (nev) -> for wst in @rgwst if wst.nev.ID() == nev.ID() return true return false ShowMenu: (ev, dLink, rgverb) -> @ClearMenu(true) jMenu = $("
").hide() for verb in rgverb dgClick = ((verbT) => () => verbT.dgActivate(); @Display())(verb) jMenuItem = $("#{verb.stDisplay}
").click(dgClick) jMenu.append(jMenuItem) jMenu.css({ position: "absolute" top: ev.pageY left: ev.pageX }) @jDiv.append(jMenu) @jMenu = jMenu @fMenuVisible = false jMenu.show('blind', {}, 300, () => @fMenuVisible = true) ClearMenu: (fForce) -> if @jMenu? and (fForce or @fMenuVisible) @jMenu.hide('blind', {}, 300, () -> $(this).remove()) @jMenu = null @fMenuVisible = false RegOnClick: (dgOnClick) -> stId = "__gen#{@igenID++}" @mpstId_dgOnClick.push([stId, dgOnClick]) stId Link: (stText, dgOnClick, stExtra) -> if not stExtra? stExtra = "" "#{stText}" RgwstRun: () -> wst = @RunNev(@rgwstInit[@rgwstInit.length - 1], @SectionCurrent().NevByName("start")) eniWst = Nevgen.EniWst(this, wst) ArrayFromEni(eniWst) RunNev: (wst, nevResponse) -> wst = wst.WstNext(nevResponse) nevResponse.RunAction(this, wst) wst FWillNevRun: (wst, eniNevResponseNext, nevgen, respo, nevPretend, nevLater) -> wstNext = @RunNev(wst, nevPretend) ThunkWstContinue = (eniWstRespondingOld) -> () -> if eniWstRespondingOld? if eniWstRespondingOld.Value() == wst return new EniCons(wst, ThunkWstContinue(null)) else return new EniCons(eniWstRespondingOld.Value(), ThunkWstContinue(eniWstRespondingOld.EniNext())) else return new EniConcat(new Eni(wstNext), eniWstNextPre) eniWstRespondingNew = ThunkWstContinue(nevgen.eniWstResponding)().EniNext() eniWstNextPre = new Nevgen(this, wstNext, eniWstRespondingNew, respo.ReplaceEniNev(eniNevResponseNext)).Thunk()() eniWstNext = eniWstNextPre.EniNext() while eniWstNext? if eniWstNext.Value().nev.ID() == nevLater.ID() return true eniWstNext = eniWstNext.EniNext() return false FilterStHtml: (stHtml, wst) -> for actor in @story.rgactor try stHtml = actor.FilterStHtml(stHtml, wst) catch error stHtml = stHtml + "

[[error interpreting markup: #{error}]]

" stHtml StHtmlUi: (wst) -> stHtmlUi = "" for actor in @story.rgactor if actor.StHtmlUi? stHtmlUiT = actor.StHtmlUi(wst) if stHtmlUiT? stHtmlUi += stHtmlUiT stHtmlUi bind = (fn, me) -> ((args...) -> fn.apply(me, args))# argh new coffeescript won't let me use __bind :( # narrative event -- the story is told as a sequence of these. Contains the HTML to display when the # narrative event happens, the preconditions to make sure the event should be allowed to occur, and # the resultant effect on the state of the model of the world class Nev @RgstelemReserved = ["cond", "set", "donext"] constructor: (@dNev) -> ID: () -> if not @id? rgstName = $(@dNev).parents("[name]").map(() -> $(this).attr("name")).get() rgstName.reverse() rgstName.push($(@dNev).attr("name")) @id = rgstName.join(".") @id FHasRun: (wst) -> while wst?.nev if wst.nev.ID() == @ID() return true wst = wst.WstPrev() return false FConds: (wst) -> for dCond in $("cond", @dNev) try if (!(new Function("wst", "with(wst) {return #{dCond.textContent};}")(wst))) return false catch error return false true FCanRun: (gst, wst) -> not @FHasRun(wst) and @FConds(wst) FEndsSection: () -> $(@dNev).attr("nextsection")? RunAction: (gst, wst) -> for dSet in $("set", @dNev) try new Function("wst", "wst.#{$(dSet).attr('var')} = #{dSet.textContent};")(wst) catch e alert("error setting #{$(dSet).attr('var')} to #{dSet.textContent}") StHtmlNextSection: (gst, wst) -> stSectionNext = $(@dNev).attr("nextsection") if stSectionNext? section = gst.story.SectionByName(stSectionNext) if section? gst.Link("Next section", () -> gst.PushSection(section, wst)) StHtmlDisplay: (gst, wst) -> jdivTmp = $("
") for dHTML in $(@dNev).contents() fReserved = false for stElemReserved in Nev.RgstelemReserved if ($.nodeName(dHTML, stElemReserved)) fReserved = true break if not fReserved jdivTmp.append(document.importNode(dHTML, true)) stHtml = jdivTmp[0].innerHTML "
#{gst.StHtmlUi(wst)}
#{gst.FilterStHtml(stHtml, wst)}
" #[word text to display] -- links to a word, usually a noun, that the player can interact with in some way. #[word] == [word word] # $(expr) inserts as a string the result of expr # $[statements] executes some javascript (semicolons needed) # [[ outputs [, ]] outputs ] TemplateFromStNev = (st, wst) -> class Xpd constructor: (@st, @fExpr) -> JsStringLit = (st) -> "'" + st.replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/, "\\t") + "'" rgxpd = [] ich = 0 ichLim = 0 ichLimTok = 0 PushText = (fExpr, dgSt) -> stTok = st.substring(ich, ichLim) if dgSt? then stTok = dgSt(stTok) if stTok then rgxpd.push(new Xpd(stTok, fExpr)) ich = ichLimTok ichLim = ichLimTok Match = (dgPushNoTok, rgre_dg...) -> ire = 0 ireMatched = -1 ichMatchFirst = null for ire in [0...rgre_dg.length] by 2 re = rgre_dg[ire] re.lastIndex = ichLimTok rgstMatched = re.exec(st) if rgstMatched? ichMatchT = re.lastIndex - rgstMatched[0].length if not ichMatchFirst? or ichMatchT < ichMatchFirst ichMatchFirst = ichMatchT stMatched = rgstMatched[0] ireMatched = ire reMatched = rgre_dg[re] if ireMatched >= 0 ichLim = ichMatchFirst ichLimTok = ichMatchFirst + stMatched.length rgre_dg[ireMatched + 1]() else ichLim = ichLimTok = st.length if dgPushNoTok? dgPushNoTok() null MatchInText = () -> EndText = () -> PushText(true, (st) -> JsStringLit(st.replace(/\[\[/g, "[").replace(/\]\]/g, "]"))) Match(EndText, /\[\[/g, () -> MatchInText, /\]\]/g, () -> MatchInText, /\[/g, () -> EndText(); MatchInWord, /\$\[/g, () -> EndText(); MatchInStmts, /\$\(/g, () -> EndText(); MatchInExpr) EndMatchBracket = (cbracketSquareNew, cbracketParenNew, dgPush) -> if cbracketSquareNew <= 0 && cbracketParenNew <= 0 dgPush() MatchInText else MatchBracket(cbracketSquareNew, cbracketParenNew, dgPush) MatchBracket = (cbracketSquare, cbracketParen, dgPush) -> () -> Match(dgPush, # error? /"/g, () -> MatchString(MatchBracket(cbracketSquare, cbracketParen, dgPush)), /\[/g, () -> MatchBracket(cbracketSquare + 1, cbracketParen, dgPush), /\]/g, () -> EndMatchBracket(cbracketSquare - 1, cbracketParen, dgPush), /\(/g, () -> MatchBracket(cbracketSquare, cbracketParen + 1, dgPush), /\)/g, () -> EndMatchBracket(cbracketSquare, cbracketParen - 1, dgPush)) MatchString = (matchAfter) -> () -> Match(null, /\\\\/g, () -> MatchString(matchAfter), /\\"/g, () -> MatchString(matchAfter), /"/g, () -> matchAfter) StWord = (st) -> rgstDisplay = /[ ]*([^ ]+)(.*)/.exec(st) stWord = rgstDisplay[1] if rgstDisplay[2] stDisplay = rgstDisplay[2] else stDisplay = rgstDisplay[1].replace(/_/g, " ") word = wst.gst.SectionCurrent().WordByName(wst.gst, stWord) if word? and (rgverb = word.Rgverb(wst)).length > 0 JsStringLit(wst.gst.Link(stDisplay, ((ev) -> wst.gst.ShowMenu(ev, this, rgverb)), "class='kliffy-word'")) else JsStringLit(stDisplay) MatchInWord = MatchBracket(1, 0, () -> PushText(true, StWord)) MatchInExpr = MatchBracket(0, 1, () -> PushText(true)) MatchInStmts = MatchBracket(1, 0, () -> PushText(false)) BuildRgxpd = (dgMatch) -> while dgMatch? dgMatch = dgMatch() BuildRgxpd(MatchInText) rgstJs = ["var __p=[];with(wst){"] fExpr = false for xpd in rgxpd if xpd.fExpr != fExpr rgstJs.push(if fExpr then ");" else "__p.push(") else if fExpr rgstJs.push(",") fExpr = xpd.fExpr rgstJs.push(xpd.st) if fExpr then rgstJs.push(");") rgstJs.push("}return __p.join('');") new Function("wst",rgstJs.join('')) class Word constructor: (@gst, @jWord) -> Rgverb: (wst) -> rgverb = [] for dVerb in @jWord.find("verb") nev = new Nev(dVerb) if not @gst.FWasRun(nev) and not @gst.story.actorPlayer.FWillAttempt(wst, nev) and nev.FConds(wst) stDisplay = $(dVerb).attr("display") or $(dVerb).attr("name") dgActivate = ((dVerbT) => () => @gst.story.actorPlayer.RespondTo(wst.nev, new Nev(dVerbT)))(dVerb) rgverb.push(new Verb(stDisplay, dgActivate)) rgverb class Verb constructor: (@stDisplay, @dgActivate) -> # actors drive the story by responding to nevs with more nevs # ActorPlayer does its best to perform the actions that the player has told the game to perform. class ActorPlayer constructor: (@story) -> @mpnevID_rgnev = {} RespondTo: (nev, nevResponse) -> nevResponse.player_nevIDRespondedTo = nev.ID(); rgnev = @mpnevID_rgnev[nev.ID()] if rgnev? rgnev.splice(0, 0, nevResponse) else @mpnevID_rgnev[nev.ID()] = [nevResponse] RemoveResponse: (nevResponse) -> rgnev = @mpnevID_rgnev[nevResponse.player_nevIDRespondedTo] for nev, inev in rgnev if nev.ID() == nevResponse.ID() rgnev.splice(inev, 1) break FWillAttempt: (wst, nev) -> rgnev = @mpnevID_rgnev[wst.nev.ID()] if rgnev? for nevResponse in rgnev if nevResponse.ID() == nev.ID() return true return false EninevResponse: (nev, wst) -> rgnev = @mpnevID_rgnev[nev.ID()] if rgnev? then new EniArray(rgnev) else EniEmpty FilterStHtml: (stHtml, wst) -> TemplateFromStNev(stHtml, wst)(wst) StHtmlUi: (wst) -> if (wst.nev.player_nevIDRespondedTo?) wst.gst.Link("Undo", (() => @RemoveResponse(wst.nev); wst.gst.Display()), "class='kliffy-undo'") class ActorNext constructor: (@story) -> @mpnevID_rgnev = {} for section in @story.Rgsection() for dDoNext in section.jSection.find("donext") @add(new Nev($(dDoNext.parentNode)), section.NevByName($(dDoNext).attr("nev"))) add: (nev, nevResponse) -> if not @mpnevID_rgnev[nev.ID()]? @mpnevID_rgnev[nev.ID()] = [] @mpnevID_rgnev[nev.ID()].push(nevResponse) EninevResponse: (nev, wst) -> rgnev = @mpnevID_rgnev[nev.ID()] if rgnev? then new EniArray(rgnev) else EniEmpty FilterStHtml: (stHtml, wst) -> stHtml class Story constructor: (@jStory) -> @actorPlayer = new ActorPlayer(this) @rgactor = [@actorPlayer, new ActorNext(this)] SectionByName: (stName) -> jSection = @jStory.find("section[name=#{stName}]") if jSection.length == 1 then new Section(this, jSection) Rgsection: () -> new Section(this, $(jSection)) for jSection in @jStory.find("section").toArray() RespoResponse: (nev, wst) -> Respo.FromEnieniNev(new EniMap(new EniArray(@rgactor), (actor) -> actor.EninevResponse(nev, wst))) WstInit: (gst) -> NewWst({}, gst) class Section constructor: (@story, @jSection) -> NevByName: (stName) -> if stName? rgstName = stName.split('.') for dNev in @jSection.find("[name=#{rgstName[rgstName.length - 1]}]") istName = rgstName.length - 2 dParent = dNev while dParent? and istName >= 0 dParent = $(dParent).parent("[name=#{rgstName[istName]}]").get(0) istName-- if dParent? return new Nev(dNev) null WordByName: (gst, stName) -> jWord = @jSection.find("word[name=#{stName}]") if jWord.length > 0 then new Word(gst, jWord) Rgget = (rgurl, dgAfter, iurl, rgresult) -> if not iurl? then iurl = 0 if not rgresult then rgresult = [] if iurl == rgurl.length dgAfter(rgresult) else # $.get, called with javascript, results in a "syntax error" in FireFox if the result is not xml. This is OK. $.get(rgurl[iurl], ((result) -> rgresult.push(result); Rgget(rgurl, dgAfter, iurl + 1, rgresult)), "text") window.PlayStory = (url, jDiv) -> $.get(url, (domXml) -> story = new Story($("story", domXml)) gst = new Gst(story, jDiv) rgurlExt = [] for domLoad in $("loadmodule", domXml) rgurlExt.push($(domLoad).attr("url")) Rgget(rgurlExt, (rgscript) -> for script in rgscript eval(script)(gst) gst.Display() ) , "xml")