diff --git a/causality.txt b/causality.txt index 1988e70..cf8a2e2 100644 --- a/causality.txt +++ b/causality.txt @@ -1,30 +1,33 @@ -TIME TRAVEL: -actions -- things that the player has explicitly done. They generate nevs, but are not necessarily the only source of them. -nev -- narrative event -- a moment in time. nevs contain some text to display to the user to communicate what is happening, some handles with which she may generate actions, and some consequences to the state of the world. - -CHANGING THE PAST: -in my game, a nev can be inserted in the past as a result of a later action, in direct violation of causality. I do this because it is interesting. -ways we could deal with this: - - try to calculate a stable loop to determine whether or not the action is legal - - cons: hard for the author to reason about (?), hard to write code to deal with, high time complexity - - split the timeline - - cons: the player may be able to perform an action which has consequences in the past which will prevent him from performing the action. The player will then see the consequence but not the action. - - model this problem as inserting an action at the earlier point to begin with - - pros: the system should probably be able to model things this way anyway - causality is not violated - - cons: maybe somewhat fragile from the author's POV - how to specify where the nev goes? - UI for undoing should be on the nev following the player's actual action, not the modelled one - same problems as timeline splitting, actually -- just giving the author the tool rather than baking it into the engine - these are important questions regardless of whether we implement things this way or not though - -OK -ACTIONS: - - happen in response to nevs! - - nevs have [generated?] stable IDs, and appear at most once in the text. (nevs can also be parameterized?) - -MULTIPLE ACTORS: - - soooo it makes sense to have multiple actors generating actions! the player-character is merely one such actor, with a simple "AI". - - other actors can generate nevs according to other criteria. - - so we could model a TimeLord actor which generates a nev in response to the player's future action? No, because the only way to tell if future actions are valid is to simulate the world! - - could provide a world-simulating primitive that action validity calls could run \ No newline at end of file +TIME TRAVEL: +actions -- things that the player has explicitly done. They generate nevs, but are not necessarily the only source of them. +nev -- narrative event -- a moment in time. nevs contain some text to display to the user to communicate what is happening, some handles with which she may generate actions, and some consequences to the state of the world. + +CHANGING THE PAST: +in my game, a nev can be inserted in the past as a result of a later action, in direct violation of causality. I do this because it is interesting. +ways we could deal with this: + - try to calculate a stable loop to determine whether or not the action is legal + - cons: hard for the author to reason about (?), hard to write code to deal with, high time complexity + - split the timeline + - cons: the player may be able to perform an action which has consequences in the past which will prevent him from performing the action. The player will then see the consequence but not the action. + - model this problem as inserting an action at the earlier point to begin with + - pros: the system should probably be able to model things this way anyway + causality is not violated + - cons: maybe somewhat fragile from the author's POV + how to specify where the nev goes? + UI for undoing should be on the nev following the player's actual action, not the modelled one + same problems as timeline splitting, actually -- just giving the author the tool rather than baking it into the engine + these are important questions regardless of whether we implement things this way or not though + +OK +ACTIONS: + - happen in response to nevs! + - nevs have [generated?] stable IDs, and appear at most once in the text. (nevs can also be parameterized?) + +MULTIPLE ACTORS: + - soooo it makes sense to have multiple actors generating actions! the player-character is merely one such actor, with a simple "AI". + - other actors can generate nevs according to other criteria. + - so we could model a TimeLord actor which generates a nev in response to the player's future action? No, because the only way to tell if future actions are valid is to simulate the world! + - could provide a world-simulating primitive that action validity calls could run + +GAME IDEAS: + "I'm thinking of a number" \ No newline at end of file diff --git a/iffy.coffee b/iffy.coffee new file mode 100644 index 0000000..2c062a2 --- /dev/null +++ b/iffy.coffee @@ -0,0 +1,450 @@ +### +Iffy - parserless interactive fiction for the web +(c)2010 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 @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) -> + @wstInit = @story.WstInit(this) + @igenID = 0 + + Display: () -> + @mpstId_dgOnClick = [] + @ClearMenu(true) + @jDiv.empty() + @jDiv.click(() => @ClearMenu()) + @jDiv.append("
+

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

+

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

+
") + for wst in @RgwstRun() + @jDiv.append($("
").append(wst.nev.StHtmlDisplay(this, wst))) + + for stId_dgOnClick in @mpstId_dgOnClick + @jDiv.find("##{stId_dgOnClick[0]}").click(stId_dgOnClick[1]) + + ShowMenu: (ev, dLink, rgverb) -> + @ClearMenu(true) + jMenu = $("
").hide() + for verb in rgverb + jMenuItem = $("#{verb.stDisplay}
").click(() => verb.dgActivate(); @Display()) + 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 + + RgwstRun: () -> + wst = @wstInit.WstNext(@story.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 + +# 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 + 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 + FCanRun: (gst, wst) -> not @FHasRun(wst) + RunAction: (gst, wst) -> + StHtmlDisplay: (gst, wst) -> + jdivTmp = $("
") + for dHTML in $(@dNev).contents() + jdivTmp.append(document.importNode(dHTML, true)) + stHtml = jdivTmp[0].innerHTML + 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] +# $[if (expr)]some text$[else]some other text$[endif] (else is optional) +# $[(expr)] inserts as a string the result of expr +#[[ 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(); MatchInExpr) + MatchBracket = (cbracket, dgPush) -> + () -> Match(dgPush, # error? + /"/g, () -> MatchString(MatchBracket(cbracket, dgPush)), + /\[/g, () -> MatchBracket(cbracket + 1, dgPush), + /\]/g, () -> if cbracket == 0 then dgPush(); MatchInText else MatchBracket(cbracket - 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.story.WordByName(wst.gst, stWord) + if word? and (rgverb = word.Rgverb(wst)).length > 0 + dgOnClick = (ev) -> wst.gst.ShowMenu(ev, this, rgverb) + + stId = wst.gst.RegOnClick(dgOnClick) + JsStringLit("#{stDisplay}") + else + JsStringLit(stDisplay) + MatchInWord = MatchBracket(0, () -> PushText(true, StWord)) + MatchInExpr = MatchBracket(0, () -> PushText(true)) + + BuildRgxpd = (dgMatch) -> + while dgMatch? + dgMatch = dgMatch() + + BuildRgxpd(MatchInText) + + rgstJs = ["var __p=[];with(obj){"] + 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("obj",rgstJs.join('')) + +class Word + constructor: (@gst, @jWord) -> + Rgverb: (wst) -> + rgverb = [] + for dVerb in @jWord.find("verb") + nev = new Nev(dVerb) + if not nev.FHasRun(wst) and not @gst.story.actorPlayer.FWillAttempt(wst, nev) + stDisplay = $(dVerb).attr("display") or $(dVerb).attr("name") + dgActivate = () => @gst.story.actorPlayer.RespondTo(wst.nev, new Nev(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) -> + rgnev = @mpnevID_rgnev[nev.ID()] + if rgnev? + rgnev.push(nevResponse) + else + @mpnevID_rgnev[nev.ID()] = [nevResponse] + 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) + +class Story + constructor: (@jStory) -> + @actorPlayer = new ActorPlayer(this) + @rgactor = [@actorPlayer] + NevByName: (stName) -> + if stName? + rgstName = stName.split('.') + for dNev in @jStory.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 = @jStory.find("word[name=#{stName}]") + if jWord.length > 0 then new Word(gst, jWord) + RespoResponse: (nev, wst) -> + Respo.FromEnieniNev(new EniMap(new EniArray(@rgactor), (actor) -> actor.EninevResponse(nev, wst))) + WstInit: (gst) -> + NewWst({}, gst) + +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") \ No newline at end of file diff --git a/retrochronal.coffee b/retrochronal.coffee new file mode 100644 index 0000000..8eba831 --- /dev/null +++ b/retrochronal.coffee @@ -0,0 +1,35 @@ +### +RetroChronal -- an iffy module for violating causality + +Usage: + + nev text + + +where the nev specified in "after" is the one that this nev should appear after, and the nev specified in future_cause is the one +that causes this nev to happen in the future. If the nev specified in future_cause doesn't appear, this nev will not be displayed. +### + +class ActorRetro + constructor: (@story) -> + @mpnevID_rgnevResponse = {} + for dResponse in @story.jStory.find("pastresponse") + @add(@story.NevByName($(dResponse).attr("after")), dResponse) + add: (nev, dResponse) -> + if not @mpnevID_rgnevResponse[nev.ID()]? + @mpnevID_rgnevResponse[nev.ID()] = [] + @mpnevID_rgnevResponse[nev.ID()].push(new Nev($(dResponse))) + EninevResponse: (nev, wst) -> + rgnevTest = @mpnevID_rgnevResponse[nev.ID()] + if rgnevTest? + FKeepNevTest = (nevTest, eniNev, nevgen, respo) -> + nevFuture = nevgen.wst.gst.story.NevByName($(nevTest.dNev).attr("future_cause")) + return nevFuture? and nevgen.wst.gst.FWillNevRun(nevgen.wst, new EniFilter(eniNev, FKeepNevTest), nevgen, respo, nevTest, nevFuture) + + new EniFilter(new EniArray(rgnevTest), FKeepNevTest) + else + EniEmpty + FilterStHtml: (stHtml, wst) -> stHtml + +return (gst) -> + gst.story.rgactor.splice(0, 0, new ActorRetro(gst.story)) \ No newline at end of file diff --git a/test.html b/test.html index 0e7806a..5397160 100644 --- a/test.html +++ b/test.html @@ -1,15 +1,31 @@ - - - - - - - - -
- - + + + + + + + + + + + +
+ + diff --git a/timemachine.xml b/timemachine.xml index daac0b4..badff1e 100644 --- a/timemachine.xml +++ b/timemachine.xml @@ -1,47 +1,46 @@ - - -

"I still can't believe you actually used a [DeLorean]," says Larry.

-

"Hey, if you're going to do a thing, you ought to do it right," says [Richard].

-
- - - Larry looks at the DeLorean with astonishment. Richard has done it up to look exactly like the car from - Back to the Future. - - -

Larry eyes the car. "Give me the keys," he says. "I want to take this baby to 88."

-

"Oh, it doesn't drive anymore," says Richard. "I had to use the engine to power the time machine."

-
-
- - -

"So, run it by me again," says Larry.

-

"I modified this [DeLorean] to send information backwards through time," says [Richard].

-

"Just information. Not matter."

-

"Right."

-

Larry ponders this for a moment. "How... how does that even work?"

-

[Richard] waves his hands around. "Spooky action at a distance," he says.

-

"No, I mean, augh. I mean, what can you practically change about the past with this machine?" says Larry.

-

- "Well, so far I've gotten my computer to crash five minutes before I push this [button]," says [Richard], gesturing at one of the - controls inside the [DeLorean]. -

-
-
- - - - -

Larry reaches for the button and gives it a push.

-

"Nothing happened", says Larry, looking at the [computer].

-

"I told you you were going to push it," says [Richard].

-

Larry opens his mouth to say something, then seems to think better of it.

-
-
- -

There is a beep as [Richard Richard's] [computer] spontaneously and inexplicably reboots.

-

"Aw, geez, you're going to push the button, aren't you?", whines [Richard].

-

"What? What button?" asks Larry, puzzled.

-

"Never mind," says [Richard].

-
-
+ + + +

"I still can't believe you actually used a [DeLorean]," says Larry.

+

"Hey, if you're going to do a thing, you ought to do it right," says [Richard].

+
+ + + Larry looks at the [DeLorean] with astonishment. [Richard] has done it up to look exactly like the car from + Back to the Future. + + +

Larry eyes the [DeLorean car]. "Give me the keys," he says. "I want to take this baby to 88."

+

"Oh, it doesn't drive anymore," says [Richard]. "I had to use the engine to power the time machine."

+
+
+ + +

"So, run it by me again," says Larry.

+

"I modified this [DeLorean] to send information backwards through time," says [Richard].

+

"Just information. Not matter."

+

"Right."

+

Larry ponders this for a moment. "How... how does that even work?"

+

[Richard] waves his hands around. "Spooky action at a distance," he says.

+

"No, I mean, augh. I mean, what can you practically change about the past with this machine?" says Larry.

+

+ "Well, so far I've gotten my computer to crash five minutes before I push this [button]," says [Richard], + gesturing at one of the controls inside the [DeLorean]. +

+
+
+ + +

Larry reaches for the button and gives it a push.

+

"Nothing happened", says Larry, looking at the [computer].

+

"I told you you were going to push it," says [Richard].

+

Larry opens his mouth to say something, then seems to think better of it.

+
+
+ +

There is a beep as [Richard Richard's] [computer] spontaneously and inexplicably reboots.

+

"Aw, geez, you're going to push the button, aren't you?", whines [Richard].

+

"What? What button?" asks Larry, puzzled.

+

"Never mind," says [Richard].

+
+