--!name Solo Oracle
--!icon dice
--!category Games
local ah,V,ap,ag=8,12,28,40 local W={"Impossible","Very unlikely","Unlikely","Somewhat unlikely","50/50","Somewhat likely","Likely","Very likely","Certain"}local an={3,12,25,38,50,62,75,88,97}local af={"Yes, and","Yes","Yes, but","No, but","No","No, and"}local aH={"Yes, and something extra goes your way.","A clear yes.","Yes, but it comes with a cost or a catch.","No, but you get something to work with.","A clear no.","No, and things get worse.",}local aE="A new face shows up|Someone you know makes a move|An enemy gains ground|An ally asks for help|".."*A thread moves forward|*A thread hits a snag|*A thread takes a sharp turn|The past comes back|".."The place itself changes|Good fortune comes your way|Trouble lands on you|A secret comes to light|".."A strange sign or omen|News arrives from far away|Something you carry matters|A hard choice appears|".."Time is running out|A moment of calm|Someone wants a deal|A rival steps in"local aB="Abandon|Accuse|Aid|Ambush|Arrive|Bargain|Betray|Break|Build|Burn|Capture|Change|Chase|Claim|".."Conceal|Corrupt|Curse|Deceive|Defend|Delay|Deliver|Demand|Destroy|Discover|Divide|Escape|Expose|".."Fail|Follow|Forgive|Gather|Guard|Guide|Harm|Heal|Hunt|Imprison|Inherit|Inspire|Invade|Lose|Lure|".."Mourn|Negotiate|Oppose|Overthrow|Persuade|Plot|Protect|Provoke|Pursue|Rebuild|Refuse|Release|".."Rescue|Return|Reveal|Ruin|Sacrifice|Search|Seize|Separate|Spy|Steal|Summon|Test|Threaten|Trade|".."Transform|Trap|Trust|Uncover|Unite|Vanish|Wake|Warn"local aJ="Alliance|Ambition|Animal|Army|Relic|Beast|Blood|Bond|Border|Bridge|Burden|Child|Coin|Crown|".."Debt|Dream|Enemy|Faith|Family|Fear|Fire|Food|Fortress|Friend|Gate|Gift|Gold|Grave|Guild|Home|Honour|".."Hope|Illness|Journey|Key|Knowledge|Land|Law|Leader|Letter|Light|Love|Machine|Magic|Map|Memory|".."Message|Monster|Mystery|Oath|Path|Plan|Power|Prisoner|Promise|Rival|Ruin|Rumour|Secret|Shelter|".."Ship|Shadow|Spirit|Storm|Stranger|Temple|Threat|Treasure|Truth|Vengeance|War|Water|Weapon|Wealth|Wound"local aC="It happens later than you planned|Someone unexpected is already there|A key person is missing|".."The place is not as you remember|It's quieter than you feared|It's more dangerous than you thought|".."You arrive in the middle of something|The weather turns against you|Something you need is gone|".."Someone got there first|What you want has moved|A friendly face is cold now|You were expected|".."Something is on fire|There's a crowd|It looks like a trap|A rival offers a deal|Your way out is blocked"local ao={"Expected","Altered","Interrupted"}local T={"d4","d6","d8","d10","d12","d20","d100","2d6","3d6","d66","4dF"}local o,p,G,M=5,0,5,6 local f,l={},{}local i,v,n,t local P,aj={},{}local b,m,k,X="menu",1,nil,nil local I local function aX(d,j)return d:match(("[^|]*|"):rep(j-1).."([^|]*)")end local function ad(d)return aX(d,math.random(select(2,d:gsub("|",""))+1))end local function L(H,aT,aO)return math.max(aT,math.min(aO,H))end local function y()local function av()local Q={"O1|"..o.."|"..p.."|"..G.."|"..M}for aq,g in ipairs(f)do Q[#Q+1]="t"..g end for aq,aP in ipairs(l)do Q[#Q+1]="j"..aP end return table.concat(Q,"\n")end local d=av()while#d>1023 and#l>0 do l[#l]=nil d=av()end snail.save(d)end local function aU()local d=snail.load()if type(d)~="string"or d:sub(1,3)~="O1|"then return end for ak in d:gmatch("[^\n]+")do local e,H=ak:sub(1,1),ak:sub(2)if e=="O"then local r,Y,E,u=ak:match("^O1|(%d+)|(%d+)|(%d+)|(%d+)")o,p=L(tonumber(r)or 5,1,9),tonumber(Y)or 0 G,M=L(tonumber(E)or 5,1,9),L(tonumber(u)or 6,1,#T)elseif e=="t"and#f<ah then f[#f+1]=H:sub(1,ap)elseif e=="j"and#l<V then l[#l+1]=H:sub(1,ag)end end end local function aa(d)table.insert(l,1,((p>0 and"S"..p.." "or"")..d):sub(1,ag))l[V+1]=nil k="Written in the journal"y()end local function Z()local A=ad(aE)local u={f=A:gsub("^%*",""),a=ad(aB),s=ad(aJ)}if A:sub(1,1)=="*"then u.th=#f>0 and f[math.random(#f)]or""end return u end local function aL()local C,w=an[G],math.random(100)local a=(5+2*o)local aA,au=math.max(1,C*a//100),math.max(1,(100-C)*a//100)local r if w<=C then r=w<=aA and 1 or w>C-aA and 3 or 2 else r=w>100-au and 6 or w<=C+au and 4 or 5 end if i then table.insert(aj,1,W[i.o]..": "..af[i.a]..(i.e and", twist"or"").." ("..i.r..")")aj[5]=nil end i={r=w,p=C,a=r,o=G}if math.random(100)<=2*o then i.e=Z()end end local function aM()p=p+1 local w,c=math.random(100),4*o n={r=w,n=p,k=w<=c and 3 or w<=8+7*o and 2 or 1}if n.k==3 then n.e=Z()elseif n.k==2 then n.alt=ad(aC)end y()end local function a_()local E=T[M]local ac,d=E:match("^(%d*)d(%w+)$")ac=tonumber(ac)or 1 local R,s,q={},0 if d=="F"then for c=1,4 do local H=math.random(3)-2 s=s+H R[c]=H>0 and"+"or H<0 and"-"or"0"end s=(s>0 and"+"or"")..s q="4dF: "..table.concat(R," ").." = "..s elseif d=="66"then local r,Y=math.random(6),math.random(6)s=r*10+Y q="d66: "..r.." then "..Y.." = "..s else for c=1,ac do R[c]=math.random(tonumber(d))s=s+R[c]end q=E..": "..(ac>1 and table.concat(R," + ").." = "or"")..s end if t then table.insert(P,1,t.x);P[5]=nil end t={t=s,x=q}end local aD="!$%&()+,-0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnpqrstuvwxyz|~"local function a1(c)return aD:sub(c+1,c+1)end local U={"1234567890<","QWERTYUIOP'","ASDFGHJKL-+","^ZXCVBNM,./","()[_____]?>"}local aI="'-+,./()?"local h local F,B,D=1,2,1 local function O(q,K)return U[K]:sub(q,q)end local function at(A,c)local d=c and(A=="t"and f or l)[c]or""h={f=A,i=c,s=d,at=#d,max=A=="t"and ap or ag-4}D=d==""and 1 or 0 if B==1 then F,B=1,2 end b="kb"end local function as()local d=(h.s:gsub("^%s+",""):gsub("%s+$",""))local A,c=h.f,h.i b,h,m=A=="t"and"threads"or"journal",nil,c or m if d==""then return end if A=="t"then if c then f[c]=d else f[#f+1]=d;m=#f end k=c and"Thread changed"or"Thread added"y()elseif c then l[c]=d;y()else aa(d);m=1 end end local function aZ()local e=O(F,B)local d,z=h.s,h.at if e=="<"then if z>0 then h.s,h.at=d:sub(1,z-1)..d:sub(z+1),z-1 end elseif e=="^"then D=(D+1)%3 elseif e=="["then h.at=math.max(0,z-1)elseif e=="]"then h.at=math.min(#d,z+1)elseif e==">"then as()elseif#d>=h.max then k="That's as long as it can be"else if e=="_"then e=" "elseif D==0 then e=e:lower()end h.s,h.at=d:sub(1,z)..e..d:sub(z+1),z+1 if D==1 and e:match("%a")then D=0 end end end local function aQ(j)if j=="top"then as()elseif j=="ok"then aZ()elseif j=="left"or j=="right"then local E,e=j=="left"and-1 or 1,O(F,B)repeat F=(F-1+E)%11+1 until e~="_"or O(F,B)~="_"elseif j=="up"or j=="down"then B=(B-1+(j=="up"and-1 or 1))%#U+1 end end local function aR()local x,aY={},O(F,B)=="_"for K=1,#U do local g={}for q=1,11 do local e=U[K]:sub(q,q)local _=K==B and(q==F or(aY and e=="_"))local N,C,c=_ and 1 or 0,aI:find(e,1,true)if e:match("%u")then c=_ and e:byte()-65 elseif e:match("%d")then c=_ and 26+e:byte()-48 elseif C then c=(_ and 35+C)or(e=="/"and 45)elseif e=="<"then c=46+N elseif e=="^"then c=48+D*2+N elseif e=="["then c=54+N elseif e=="]"then c=56+N elseif e=="_"then c=58+N*3+(O(q-1,K)~="_"and 0 or O(q+1,K)~="_"and 2 or 1)else c=64+N end g[q]=c and a1(c)or("{"..e.."}")end x[K]=table.concat(g)end return table.concat(x,"/")end local am={"ask","event","scene","dice","threads","journal","tension","help"}local function ae()if b=="menu"then return am end if b=="ask"then return i and{"odds","tension","log"}or{"odds","tension"}end if b=="event"then return{"again","log"}end if b=="scene"then return n and{"next","calm","wild","log"}or{"next","calm","wild"}end if b=="dice"then return t and{"die","log"}or{"die"}end if b=="threads"or b=="journal"then local g={}for c=1,#(b=="threads"and f or l)do g[c]=c end if b=="threads"and#f<ah then g[#g+1]="add"end if b=="journal"then g[#g+1]="add"end if b=="threads"and#f>1 then g[#g+1]="random"end return g end if b=="thread"then return{"edit","top","close"}end if b=="entry"then return{"edit","delete"}end return{}end local function a0(a)if type(a)=="number"then return b=="threads"and f[a]or l[a]end if a=="ask"then return"Ask a yes/no question"end if a=="event"then return"Random event"end if a=="scene"then return p>0 and"Scene check (scene "..p..")"or"Scene check"end if a=="dice"then return"Roll dice"end if a=="threads"then return"Threads ("..#f..")"end if a=="journal"then return"Journal ("..#l..")"end if a=="tension"then return"Tension: "..o.." of 9"end if a=="help"then return"How it works"end if a=="odds"then return"Odds: "..W[G].." ("..an[G].."% yes)"end if a=="log"then return"Write it in the journal"end if a=="again"then return"Roll another event"end if a=="next"then return"Start scene "..p+1 end if a=="calm"then return"Scene went my way: tension -1"end if a=="wild"then return"Scene got away from me: tension +1"end if a=="die"then return"Dice: "..T[M]end if a=="add"then return b=="threads"and"+ Add a thread"or"+ Write an entry"end if a=="random"then return"Pick a thread at random"end if a=="edit"then return"Change the words"end if a=="top"then return"Move to the top"end if X==a then return"Press OK again to "..a end return a=="close"and"Close this thread"or"Delete this entry"end local function aw(E)o=L(o+E,1,9)k="Tension "..o y()end local function aV()if b=="ask"then aa(W[i.o].."? "..af[i.a]..(i.e and", twist"or""))elseif b=="event"then aa(v.a.." / "..v.s)elseif b=="scene"then local d="Scene "..n.n..": "..ao[n.k]table.insert(l,1,d)l[V+1]=nil k="Written in the journal"y()else aa(t.x)end end local function aN(d)b,m=d,1 if d=="event"then v=Z()end end local function aK(a,j,r)local ab=(j=="left"and-1)or(j=="right"and 1)if ab then if a=="tension"then aw(ab)elseif a=="odds"then G=L(G+ab,1,9);y()elseif a=="die"then M=(M-1+ab)%#T+1 if t then table.insert(P,1,t.x);P[5]=nil end t=nil y()end return end if j~="ok"then return end if b=="menu"then if a=="tension"then k="LEFT and RIGHT change the tension"else aN(a)end elseif a=="odds"or a=="tension"then aL()elseif a=="log"then aV()elseif a=="again"then v=Z()elseif a=="next"then aM()elseif a=="calm"or a=="wild"then aw(a=="calm"and-1 or 1)elseif a=="die"then a_()elseif type(a)=="number"then I,m=a,1 b=b=="threads"and"thread"or"entry"elseif a=="add"then at(b=="threads"and"t"or"j")elseif a=="random"then k="Thread: "..f[math.random(#f)]elseif a=="edit"then at(b=="thread"and"t"or"j",I)elseif a=="top"then table.insert(f,1,table.remove(f,I))b,m="threads",1 y()elseif r~=a then X=a else table.remove(a=="close"and f or l,I)k=a=="close"and"Thread closed"or"Entry deleted"b,m=a=="close"and"threads"or"journal",1 y()end end local ai={ask="menu",event="menu",scene="menu",dice="menu",threads="menu",journal="menu",help="menu",thread="threads",entry="journal"}function key(j)if b=="kb"then k=nil;aQ(j);return end if b=="menu"and j=="top"then return end k=nil local r=X X=nil local g=ae()if j=="top"then if ai[b]=="menu"then for c,a in ipairs(am)do if a==b then m=c end end else m=I end b=ai[b]return end if#g==0 then return end if j=="up"then m=(m-2)%#g+1 elseif j=="down"then m=m%#g+1 else aK(g[m],j,r)end m=L(m,1,#ae())end local S=("...................."):rep(3,"/")local function x(al)local g=ae()local az=math.max(1,math.min(m-al//2,#g-al+1))for c=az,math.min(#g,az+al-1)do snail.row(a0(g[c]):sub(1,90),c==m)end end local function aW()snail.board(("#"):rep(o)..("o"):rep(9-o),"small")end local function ar(g)if#g>0 then snail.rule()end for c=1,#g do snail.small("Before: "..g[c])end end local function ay()if#f==0 then return end snail.rule()for c=1,math.min(4,#f)do snail.small("Thread: "..f[c])end end local function ax(u,aS)snail.text(aS..u.f)if u.th then snail.text(u.th~=""and"Thread: "..u.th or"(No threads yet: use your current goal)")end snail.text("Think: "..u.a.." / "..u.s)end local aG={odds="BACK menu  OK ask  L/R odds  UP/DN move",tension="BACK menu  OK ask  L/R tension  UP/DN move",die="BACK menu  OK roll  L/R dice  UP/DN move",}local aF={"Ask: pick the odds and press OK. An 'and' makes the answer stronger, a 'but' softens it.","Tension, 1 to 9, is how wild the story is. Higher: more twists, extreme answers and odd scenes.","Scene check: one as each scene starts. Altered: change one thing. Interrupted: play the event.","Random event: read the focus and the two words and let them suggest what happens.","Threads are your open goals and mysteries; events can pick one. The journal keeps short notes.",}function draw()local a=ae()[m]if b=="kb"then snail.title(h.i and"Change the words"or h.f=="t"and"New thread"or"Journal entry")snail.status(k or(D==2 and"CAPS"or D==1 and"Shift"or""))snail.text(h.s:sub(1,h.at).."|"..h.s:sub(h.at+1))snail.small(h.f=="t"and"A goal, a mystery or a loose end, like Find the lost heir."or"A short note. Up to "..h.max.." letters; the scene number is added.")snail.board(aR())snail.hint("arrows move  BACK done  OK press")return end if b=="menu"then snail.title("Solo Oracle")snail.status(k or(p>0 and"Scene "..p.."  -  tension "..o or"Tension "..o))x(12)aW()snail.hint(a=="tension"and"BACK leave  L/R change  UP/DN move"or"BACK leave  OK choose  UP/DN move")return end if b=="ask"then snail.title("Ask the oracle")snail.status(k or"Tension "..o)if i then snail.board(S,nil,af[i.a]:upper())snail.text("Rolled "..i.r..", yes on "..i.p.." or less ("..W[i.o]..")")if i.a~=2 and i.a~=5 then snail.small(aH[i.a])end if i.e then ax(i.e,"Twist! ")end else snail.small("Think of a yes/no question, set the odds, then press OK.")end x(6)ar(aj)elseif b=="event"then snail.title("Random event")if k then snail.status(k)end snail.board(S,nil,v.f)if v.th then snail.text(v.th~=""and"Thread: "..v.th or"(No threads yet: use your current goal)")end snail.text("Think: "..v.a.." / "..v.s)x(6)if not v.th then ay()end elseif b=="scene"then snail.title(p>0 and"Scene "..p or"Scene check")snail.status(k or"Tension "..o)if n then snail.board(S,nil,ao[n.k]:upper())snail.text("Rolled "..n.r.." at tension "..o)if n.k==1 then snail.small("Play the scene as you pictured it.")elseif n.k==2 then snail.text("Change one thing: "..n.alt)else ax(n.e,"Instead: ")end else snail.small("As each scene starts, say what you expect to happen, then check it.")end x(6)if not(n and n.e and n.e.th)then ay()end elseif b=="dice"then snail.title("Dice")if k then snail.status(k)end if t then snail.board(S,nil,t.t)snail.text(t.x)end x(4)ar(P)elseif b=="threads"or b=="journal"then local J=b=="threads"snail.title(J and"Threads"or"Journal")snail.status(k or(J and#f.." of "..ah or#l.." of "..V..", newest first"))x(14)if J and#f==0 then snail.small("Threads are open goals and mysteries. Random events can pick one.")end if not J and#l==0 then snail.small("Short notes as you play. Results can be written here too.")end elseif b=="thread"or b=="entry"then local J=b=="thread"snail.title(J and"Thread"or"Journal entry")if k then snail.status(k)end snail.text(J and f[I]or l[I])x(4)else snail.title("How it works")for aq,d in ipairs(aF)do snail.text(d)end snail.hint("BACK menu")return end snail.hint(aG[a]or((a=="close"or a=="delete")and"BACK back  OK twice  UP/DN move")or"BACK "..(ai[b]=="menu"and"menu"or"back").."  OK choose  UP/DN move")end function start()math.randomseed(os.time())aU()end
