--!name Gin Rummy
--!icon cards
--!category Games
local bt="!$%&()+,-0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnpqrstuvwxyz|~"local function S(a)return bt:sub(a+1,a+1)end local bA,bw,a3,a5,bx,bs,a4=13,17,21,23,25,26,29 local a7={"A","2","3","4","5","6","7","8","9","10","J","Q","K"}local a8={[0]="clubs","diamonds","spades","hearts"}local bv={[0]="You","Fox"}local bz={2200,1300,700}local by={"relaxed","normal","fast"}local aD=100 local function a0(b)return b%13<10 and b%13+1 or 10 end local function U(b)return a7[b%13+1].." "..a8[b//13]end local function at(y,n)return y%13*4+y//13<n%13*4+n//13 end local function P(a)return 1<<(a-1)end local ao={speed=2}local k={gp=0,gw=0,hp=0,hw=0,gin=0,under=0,best=0}local j={[0]={},{}}local z={[0]={},{}}local L,A={},{}local aq,am={},{}local B,u,r={[0]=0,0},{[0]=0,0},{[0]=0,0}local g="none"local V,F,R,K,N,al,s=1,0,0,1,-1,-1,1 local d={k=0,kind="",kdw=0,ddw=0,pts=0,w=0}local ab,aN=99,99 local H={}local C,M,aA="menu",1,1 local an local function b_(c)for a=#c,2,-1 do local Q=math.random(a)c[a],c[Q]=c[Q],c[a]end end local function J(h)H[#H+1]=h if#H>4 then table.remove(H,1)end end local function bT()local h=""for a=#H,1,-1 do local c=h==""and H[a]or(H[a].." "..h)if#c>92 then break end h=c end return h end local function aV(c,b)for a=1,#c do if c[a]==b then return a end end end local function a2(e,a)local c={}for Q=1,#e do if Q~=a then c[#c+1]=e[Q]end end return c end local function as()return(g=="first"or g=="draw"or g=="discard")and F==1 end local function bn(m)return m..(m==1 and" point"or" points")end local function aY(e,av)local D,aL,aC={},{},{}for a=1,#e do D[a]={}aC[e[a]]=a end local function _(l,bO)local a=1 while l&P(a)==0 do a=a+1 end local c=D[a]c[#c+1]=l if bO then aL[l]=true end end for p=0,12 do local n={}for h=0,3 do local a=aC[h*13+p]if a then n[#n+1]=P(a)end end if#n==4 then _(n[1]|n[2]|n[3]|n[4])_(n[1]|n[2]|n[3])_(n[1]|n[2]|n[4])_(n[1]|n[3]|n[4])_(n[2]|n[3]|n[4])elseif#n==3 then _(n[1]|n[2]|n[3])end end for h=0,3 do for bY=0,10 do local l,aX=0,0 for p=bY,12 do local a=aC[h*13+p]if not a then break end l,aX=l|P(a),aX+1 if aX>=3 then _(l)end end end end for w,W in ipairs(av or{})do local l=0 for w,b in ipairs(W)do if not aC[b]then l=-1;break end l=l|P(aC[b])end if l>0 then _(l,true)end end return D,aL end local function ap(e,D,x,bF)if x==0 then return 0 end local a=1 while x&P(a)==0 do a=a+1 end local T,a6=bF or 999,D[a]for f=1,#a6 do if T==0 then return 0 end local l=a6[f]if l&x==l then local G=ap(e,D,x&~l,T)if G<T then T=G end end end local G=a0(e[a])if G<T then G=G+ap(e,D,x&~P(a),T-G)if G<T then T=G end end return T end local function aU(e,av)return ap(e,aY(e,av),(1<<#e)-1)end local function bf(e)local D,aR,i=aY(e),(1<<#e)-1,{}for a=1,#e do i[a]=ap(e,D,aR&~P(a))end return i end local function ai(e,av)local D,aL=aY(e,av)local x=(1<<#e)-1 local ah=ap(e,D,x)local aM=ah local af,Y,aG={},{},{}while x~=0 do local a=1 while x&P(a)==0 do a=a+1 end local aB for w,l in ipairs(D[a])do if l&x==l and ap(e,D,x&~l,aM+1)==aM then aB=l break end end if aB then local c={}for Q=1,#e do if aB&P(Q)~=0 then c[#c+1]=e[Q]end end table.sort(c,at)if aL[aB]then for w,b in ipairs(c)do Y[#Y+1]=b end else af[#af+1]=c end x=x&~aB else aG[#aG+1]=e[a]aM=aM-a0(e[a])x=x&~P(a)end end table.sort(af,function(y,n)return at(y[1],n[1])end)table.sort(Y,at)table.sort(aG,at)local ag,I={},{}for f,c in ipairs(af)do for w,b in ipairs(c)do ag[#ag+1],I[#I+1]=b,f end end for w,b in ipairs(Y)do ag[#ag+1],I[#I+1]=b,-1 end for w,b in ipairs(aG)do ag[#ag+1],I[#I+1]=b,0 end return ag,I,ah,af end local function br(af)local W={}for w,l in ipairs(af)do local y,n=l[1],l[#l]if y%13==n%13 then if#l==3 then for h=0,3 do local b=h*13+y%13 if not aV(l,b)then W[#W+1]={b}end end end else local h,c=y//13,{}for p=y%13-1,0,-1 do c[#c+1]=h*13+p W[#W+1]=table.move(c,1,#c,1,{})end c={}for p=n%13+1,12 do c[#c+1]=h*13+p W[#W+1]=table.move(c,1,#c,1,{})end end end return W end local function aP()local b=j[0][s]j[0],z[0],aN=ai(j[0])s=aV(j[0],b)or 1 end local function aQ()local e=j[0]if s>#e then s=#e end if s<1 then s=1 end ab=#e==11 and aU(a2(e,s))or aN end local function bh()local m=#L return m>20 and 10 or(m>12 and 7 or 4)end local function bD(e,bC)local aT,ac,i=999,1,bf(e)for a=1,#e do if e[a]~=bC and i[a]<aT then aT,ac=i[a],a end end return aT,ac end local function ba()local O=A[#A]if not O then return false end local e=j[1]local bG=aU(e)local c={O}for w,b in ipairs(e)do c[#c+1]=b end local a1,ac=bD(c,O)if a1>=bG then return false end if a1<=bh()then return true end local b1=a2(c,ac)return a1<aU(a2(b1,1))+a0(O)end local function bH(b)local p,h,i=b%13,b//13,0 for w,c in ipairs(aq)do if c%13==p then i=i+6 elseif c//13==h and math.abs(c%13-p)<=2 then i=i+4 end end for w,c in ipairs(am)do if c%13==p then i=i-3 elseif c//13==h and math.abs(c%13-p)==1 then i=i-1.5 end end return i end local function bB()local e=j[1]local q=0.5+(31-#L)/40 local ac,bd,bg,aw,bi,aR=1,1e9,1,99,1e9,bf(e)for a,b in ipairs(e)do if b~=N then local i,au=aR[a],0 for Q,v in ipairs(e)do local aI=math.abs(v%13-b%13)if Q==a then aI=99 end if aI==0 then au=au+1 elseif v//13==b//13 and aI<=2 then au=au+(aI==1 and 1 or 0.6)end end local h=i+1.2*au+q*bH(b)+math.random()*0.01 if h<bd then ac,bd=a,h end if i<aw or(i==aw and h<bi)then bg,aw,bi=a,i,h end end end if aw==0 or aw<=bh()then return bg,true end return ac,false end local function bb()local f=d.k local bW,l,w,aJ=ai(j[f])j[f],z[f]=bW,l j[1-f],z[1-f]=ai(j[1-f],(d.kind=="knock"or d.kind=="under")and br(aJ)or nil)end local function bL()local q=B[0]>=aD and 0 or 1 local aK=1-q r[q]=B[q]+100+25*u[q]r[aK]=B[aK]+25*u[aK]if u[aK]==0 then r[q]=r[q]*2 end g="over"k.gp=k.gp+1 if q==0 then k.gw=k.gw+1 if r[0]-r[1]>k.best then k.best=r[0]-r[1]end end end local function bZ(f)local i=1-f local aJ local aa j[f],z[f],aa,aJ=ai(j[f])d.k,d.kdw=f,aa local ad if aa==0 then j[i],z[i],ad=ai(j[i])d.kind,d.w,d.pts="gin",f,25+ad J(f==0 and"You go gin!"or"Fox goes gin!")else j[i],z[i],ad=ai(j[i],br(aJ))J(f==0 and("You knock with "..aa..".")or("Fox knocks with "..aa.."."))local Y={}for a,b in ipairs(j[i])do if z[i][a]==-1 then Y[#Y+1]=a7[b%13+1]..a8[b//13]:sub(1,1):upper()end end if#Y>0 then J((i==0 and"You lay off "or"Fox lays off ")..table.concat(Y," ")..".")end if ad>aa then d.kind,d.w,d.pts="knock",f,ad-aa else d.kind,d.w,d.pts="under",i,aa-ad+25 J("Undercut!")end end d.ddw=ad local q=d.w B[q]=B[q]+d.pts u[q]=u[q]+1 J((q==0 and"You score "or"Fox scores ")..d.pts..".")k.hp=k.hp+1 if q==0 then k.hw=k.hw+1 if d.kind=="gin"then k.gin=k.gin+1 end if d.kind=="under"then k.under=k.under+1 end end g="show"if B[q]>=aD then bL()end end local function b3()d.k,d.kind,d.w,d.pts,d.kdw,d.ddw=0,"wash",-1,0,0,0 bb()k.hp=k.hp+1 g="show"J("Two cards left in the stock: the hand is a draw.")end local function bl()if d.kind~="wash"then V=1-V end local i={}for b=0,51 do i[b+1]=b end b_(i)j[0],j[1]={},{}for a=1,10 do j[0][a]=i[a]j[1][a]=i[10+a]end table.sort(j[1],at)A={i[21]}L={}for a=22,52 do L[#L+1]=i[a]end aq,am,z[1]={},{},{}N,al,s,K=-1,-1,1,1 d.kind=""g,F,R="first",1-V,1 aP()aQ()H={}J((V==0 and"Your deal"or"Fox deals")..": the upcard is "..U(A[1])..".")end local function aH(o,bM)local b if bM then b=table.remove(A)N=b if o==0 then aq[#aq+1]=b end J((o==0 and"You take "or"Fox takes ")..U(b)..".")else b=table.remove(L)N=-1 J(o==0 and("You draw "..U(b)..".")or"Fox draws from the stock.")end table.insert(j[o],b)g,R="discard",0 if o==0 then al=b s=1 aP()for a=#j[0],1,-1 do if j[0][a]~=N then s=a;break end end if z[0][s]~=0 and b~=N then s=aV(j[0],b)end aQ()end end local function bm(o)J(o==0 and"You pass."or"Fox passes.")if R==1 then F,R=1-o,2 else g,F,R,K="draw",1-V,3,1 end end local function be(o,a,aW)local b=table.remove(j[o],a)A[#A+1]=b N=-1 if o==0 then al=-1 am[#am+1]=b aP()end if aW then J((o==0 and"You discard "or"Fox discards ")..U(b).." face down.")bZ(o)return end J((o==0 and"You discard "or"Fox discards ")..U(b)..".")if#L<=2 then return b3()end g,F,K="draw",1-o,1 end local function ak(c)local h={}for a=1,#c do h[a]=string.char(65+c[a])end return table.concat(h)end local function aj(h)local c={}for a=1,#(h or"")do local b=h:byte(a)-65 if b<0 or b>51 then return{}end c[a]=b end return c end local function bo()local t={"G1",ao.speed,k.gp,k.gw,k.hp,k.hw,k.gin,k.under,k.best}if g~="none"then local ae={g,V,F,R,K,N,al,s,B[0],B[1],u[0],u[1],d.k,d.kind,d.kdw,d.ddw,d.pts,d.w,r[0],r[1],ak(j[0]),ak(j[1]),ak(L),ak(A),ak(aq),ak(am),table.concat(H,"|")}for w,G in ipairs(ae)do t[#t+1]=G end end snail.save(table.concat(t,";"))end local function bS()local h=snail.load()if type(h)~="string"or h:sub(1,3)~="G1;"then return end local t={}for G in(h..";"):gmatch("([^;]*);")do t[#t+1]=G end local function m(a,bR,bN,bI)local v=math.tointeger(tonumber(t[a]))if not v or v<bR or v>bN then return bI end return v end ao.speed=m(2,1,3,2)k.gp,k.gw,k.hp,k.hw=m(3,0,1e9,0),m(4,0,1e9,0),m(5,0,1e9,0),m(6,0,1e9,0)k.gin,k.under,k.best=m(7,0,1e9,0),m(8,0,1e9,0),m(9,0,1e6,0)local Z=t[10]if#t<36 or not(Z=="first"or Z=="draw"or Z=="discard"or Z=="show"or Z=="over")then return end V,F,R,K=m(11,0,1,0),m(12,0,1,0),m(13,0,3,0),m(14,1,2,1)N,al,s=m(15,-1,51,-1),m(16,-1,51,-1),m(17,1,11,1)B[0],B[1],u[0],u[1]=m(18,0,999,0),m(19,0,999,0),m(20,0,99,0),m(21,0,99,0)d.k,d.kind,d.kdw,d.ddw,d.pts,d.w=m(22,0,1,0),t[23],m(24,0,999,0),m(25,0,999,0),m(26,0,999,0),m(27,-1,1,-1)r[0],r[1]=m(28,0,9999,0),m(29,0,9999,0)j[0],j[1],L,A=aj(t[30]),aj(t[31]),aj(t[32]),aj(t[33])aq,am=aj(t[34]),aj(t[35])local bp,ah={},0 for w,c in ipairs({j[0],j[1],L,A})do for w,b in ipairs(c)do if bp[b]then ah=-99 end bp[b]=true ah=ah+1 end end local ax,ay=#j[0],#j[1]local az=ah==52 and(ax==10 or ax==11)and(ay==10 or ay==11)and ax+ay<=21 az=az and(Z~="discard"or(F==0 and ax==11 or F==1 and ay==11))az=az and((Z~="show"and Z~="over")or(ax==10 and ay==10))if not az then g="none"return end H={}for c in(t[36]or""):gmatch("[^|]+")do H[#H+1]=c end g=Z if g=="show"or g=="over"then bb()else z[1]={}aP()aQ()end end local function aZ()if C=="game"and as()then snail.tick(bz[ao.speed])else snail.tick(0)end end local function ar()bo()aZ()end local function bk()for o=0,1 do B[o],u[o],r[o]=0,0,0 end V,d.kind=math.random(0,1),""bl()C="game"ar()end local function a9()if g=="first"then if ba()then aH(1,true)else bm(1)end elseif g=="draw"then aH(1,R~=3 and ba())else local a,aW=bB()be(1,a,aW)end ar()end local function bj()local c={}if g~="none"then c[#c+1]={"Continue the game","resume"}end c[#c+1]={"New game","new"}c[#c+1]={"Computer speed: "..by[ao.speed],"speed"}c[#c+1]={"Statistics","stats"}c[#c+1]={"How to play","help"}return c end local function bV(f)local X=bj()if M>#X then M=#X end if f=="up"then M=(M-2)%#X+1;return end if f=="down"then M=M%#X+1;return end local y=X[M][2]local i=f=="left"and-1 or((f=="right"or f=="ok")and 1 or 0)if i==0 then return end if y=="speed"then ao.speed=(ao.speed-1+i)%3+1 bo()elseif f=="ok"then if y=="resume"then C="game";aZ()elseif y=="new"then bk()else C,aA=y,1 end end end function key(f)an=nil if C=="menu"then return bV(f)end if C~="game"then if C=="help"and(f=="left"or f=="right")then aA=3-aA;return end if f=="top"or f=="ok"then C="menu"end return end if f=="top"then C,M="menu",1 snail.tick(0)return end if g=="show"or g=="over"then if f=="ok"then if g=="over"then bk()else bl();ar()end end return end if as()then if f=="ok"then a9()end return end if g=="first"or g=="draw"then if f=="left"or f=="right"then if R==3 then an="Both passed the upcard: draw from the stock"else K=3-K end elseif f=="ok"then if g=="first"then if K==2 then aH(0,true)else bm(0)end else aH(0,K==2)end ar()end return end local e=j[0]if f=="left"or f=="right"then s=(s+(f=="left"and-2 or 0))%#e+1 aQ()elseif f=="ok"or f=="up"then if e[s]==N then an="You can't discard the card you just took"return end if f=="up"and ab>10 then an="Knocking needs 10 or less deadwood"return end be(0,s,f=="up")ar()end end function tick()if C=="game"and as()then a9()else snail.tick(0)end end local function bE()local ae={}for p=1,11 do ae[p]={".",".",".",".",".",".",".",".",".",".",".",".","."}end local function E(p,b,h)ae[p+1][b+1]=h end local function aF(p,b,v,bU)if v<0 then E(p,b,S(a3))E(p+1,b,S(a3+1))else E(p,b,S(v%13))E(p+1,b,S((bU and bw or bA)+v//13))end end local function bc(p,aE,I)for a=1,#I do local l=I[a]if l>0 then E(p,aE+a-1,S(bs+((I[a-1]~=l)and 0 or(I[a+1]~=l and 2 or 1))))end end end local aO=g=="show"or g=="over"E(0,0,S(as()and a5+1 or a5))for a,v in ipairs(j[1])do aF(0,1+a,aO and v or-1,aO and z[1][a]==-1)end if aO then bc(2,2,z[1])if d.kind~="wash"then E(1,0,"{"..(d.k==1 and d.kdw or d.ddw).."}")E(8,0,"{"..(d.k==0 and d.kdw or d.ddw).."}")end else aF(3,5,-1)E(4,4,"{"..#L.."}")local O=A[#A]if O then aF(3,7,O)else E(3,7,S(a4))E(4,7,S(a4+1))end if(g=="first"or g=="draw")and F==0 then E(5,K==1 and 5 or 7,S(bx))end E(8,0,"{"..(g=="discard"and F==0 and ab or aN).."}")end local e=j[0]local aE=(13-#e)//2 for a,v in ipairs(e)do local b2=g=="discard"and F==0 and a==s aF(b2 and 7 or 8,aE+a-1,v,v==al or(aO and z[0][a]==-1))end bc(10,aE,z[0])for p=1,11 do ae[p]=table.concat(ae[p])end return table.concat(ae,"/")end local function bq()local f,q=d.k,d.w if d.kind=="wash"then return"A draw: nobody scores"end if d.kind=="gin"then return(f==0 and"Gin! You score "or"Fox goes gin: ")..bn(d.pts)end if d.kind=="under"then return(q==0 and"Undercut! You score "or"Fox undercuts: ")..bn(d.pts)end return(f==0 and"You knock and score "or"Fox knocks and scores ")..d.pts end local function b0()if an then return an end if g=="over"then local q=r[0]>r[1]and 0 or 1 return(q==0 and"You win "or"Fox wins ")..r[q].." to "..r[1-q]end if g=="show"then return bq()end if F==1 then return"Fox's turn..."end local O=A[#A]if g=="first"then return"Upcard "..U(O)..": take it or pass"end if g=="draw"then if R==3 or not O then return"Draw from the stock"end return"Draw, or take the "..U(O)end local b=j[0][s]if b==N then return"Discard: not the card you just took"end if ab==0 then return"Discard "..U(b).." and go gin!"end return"Discard "..U(b)..": deadwood "..ab..(ab<=10 and", can knock"or"")end local function a_()return"Score: you "..B[0]..", Fox "..B[1].." (to "..aD.."). Hands won: "..u[0].."-"..u[1]end local function bK()snail.title("Gin Rummy - "..(V==0 and"your deal"or"Fox's deal"))snail.status(b0())snail.text(bT())local aS if g=="over"then aS=r[0]>r[1]and"You win the game!"or"Fox wins the game"elseif g=="show"then aS=bq()end snail.board(bE(),nil,aS)if g=="over"then local h={}for o=0,1 do local q=r[o]>r[1-o]h[#h+1]=bv[o].." "..B[o]..(q and" + 100 game"or"").." + "..25*u[o].." hands"..((q and u[1-o]==0)and", doubled"or"").." = "..r[o]end snail.small(h[1].."; "..h[2])snail.small("Hands won: you "..u[0]..", Fox "..u[1]..". Game to "..aD..".")elseif g=="show"then if d.kind=="wash"then snail.small("Nobody knocked before the stock ran low.")else local bQ=d.k==0 and"You"or"Fox"local bJ=d.k==0 and"Fox"or"you"snail.small(bQ..(d.kind=="gin"and" went gin, "or(" knocked with "..d.kdw..", "))..bJ.." had "..d.ddw..(d.kind=="gin"and""or" after layoffs")..".")end snail.small(a_())else snail.small("Your deadwood: "..aN..". Stock: "..#L.." cards. Fox holds "..#j[1]..".")snail.small(a_())end if g=="over"then snail.hint("BACK options OK new game")elseif g=="show"then snail.hint("BACK options OK next hand")elseif as()then snail.hint("BACK options OK hurry")elseif g=="first"then snail.hint(K==2 and"BACK options OK take L/R choose"or"BACK options OK pass L/R choose")elseif g=="draw"then snail.hint(K==2 and"BACK options OK take L/R pile"or"BACK options OK draw L/R pile")elseif ab==0 then snail.hint("BACK options OK discard L/R card UP gin")elseif ab<=10 then snail.hint("BACK options OK discard L/R card UP knock")else snail.hint("BACK options OK discard L/R card")end end local bu={{"Make melds: three or four of a kind, or three or more in a row of one suit (aces low).","Each turn, draw from the stock or take the top discard, then discard a card.","Deadwood is every card not in a meld: aces 1, faces 10, the rest their number.","Knock (UP) when the discard leaves 10 or less deadwood. All of it melded is gin.","After a knock the other hand lays off cards on the knocker's melds. Yours are done for you.",},{"A knock scores the deadwood difference. If the other hand has as little: undercut, 25 more.","Gin scores 25 plus the other hand's deadwood, with no laying off.","At 100 the game ends: 100 more for the winner and 25 a hand won. A shutout doubles it all.","First turn: the upcard is offered to the non-dealer, then the dealer. The stock means pass.","Brackets mark your melds; a dark suit is the card just drawn, or one laid off.",},}function draw()if C=="game"then return bK()end if C=="stats"then snail.title("Statistics")local bX=k.gp>0 and(k.gw*100//k.gp)or 0 snail.text("Hands played: "..k.hp..", won: "..k.hw)snail.text("Your gins: "..k.gin..", undercuts: "..k.under)snail.gap()snail.text("Games played: "..k.gp)snail.text("Games won: "..k.gw.." ("..bX.."%)")snail.text("Best winning margin: "..(k.best>0 and k.best or"none yet"))snail.hint("BACK back OK back")return end if C=="help"then snail.title("How to play ("..aA.." of 2)")for w,h in ipairs(bu[aA])do snail.text(h)end snail.hint("BACK back OK back L/R page")return end snail.title("Gin Rummy")local X=bj()if M>#X then M=#X end for a,bP in ipairs(X)do snail.row(bP[1],a==M)end snail.gap()if g~="none"then snail.small(a_())else snail.small("Meld, knock and go gin against Fox. First to 100.")end snail.hint("BACK leave OK choose L/R change UP/DN move")end function start()math.randomseed(os.time())bS()if g~="none"then C="game"aZ()end end
