,"","","""",",,""",""""","wwpw wp ww}wwwww}www}www}www}www}www}wwwwwww}wwwwwwwwwwwwwwwww~wwwwwwwwwwwwwwwwwwwwwwwwwwwww~www~wwwwww~wwwwwwwwwwwwwwwwwwwww~www~~ }www}www}www}www}www}www}www}wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww~~~~ww~wwwwwwwwwwwwwwwwwwwwww~~~ww~wwwwwwwwwww p wpw}www}www}www}www}wwwwwww}wwwwwwwwwwwwwwwwwwwww}www}www pw wp ID @D ww))"""pw wp , ww  pw wp̙ "  "" ww @@E@PE@@P@pw wpU T P  @ ww, ` ` ""pw wp f f "`f " wwkfpw wpk f kf fb +f ww:333303303pw wp : 30 0   ™" ) ) ,, ") "," "" "" "" " """ """"""", " " ," ," "" " E@@@PD@@P@@U@ PD D@ T  ET   "f `" "` "&f   ""    "  fKKfKk&d+KD$DD$f "K " "B f+ d$B + B$B    ww   p wpw 0 30<30ww  00p wpw"" "" "" """™ww , ) "p wpwE@@DU@@ Pww@T T PD  @ p wpw```` ww`f f ff ` p wpw))3933333<33ww2#2 2#2 3#2 333 333 33p wpwww   p wpwi 7 7 6 5554444456789:;==>>>?????????=;:87 6 6 5 5 5 6 9 ;= > ? ? ???????????3     ?ڪݪݪڪݭڭڭݪڪڭڪڪݭݭݪݭڭڪݪڪڪڪڪڪڭݭڪڭݭڪڭڭݪڪݭڪݭڭݭڪݪݭڪݪݪڭڪݪݭڭݪݭݭڭݪݭݪڭݭڭݭڪݭڭݭܭݪڭڭݪڭڭݪڪڭڪڪͬ\Uݭڭݪڪݭݭݪݭڪݪݭݪݪݪݪڭݭ̬ڪݭڪڪڪڪڪڭڪݭݪݪݭMݪݭݭڭݪݪڪڪݭګڪݭ\ݭڪݪݪڪڭ۪ڭڪݭڪݭڪڪݺݭڪݼݫڻݪۭݭڪڭڪݫںڪݪݪݪڪڭڭ۫ݪݪڽݭݪݭݭڽݭݭڪګ۽ګ۪ڪݺݭڭݪݭ̬ݽݪ۽ݽݪګݭڭݭݪݭڭݪݫU۪ۭۭͫ۽ݭڭڽۭݪڪ۫ݽݭںګںݭݪګݫ۪۪ݪڭۭڭڭݫݻݻḼLڽڭݺڪڪݭ۽ګݪڪڪͬݫڻۭڭݫݻڭګݫ۫ڪݭݪ̬ݭݭݭݭڽݭݭݭݽݭݭݽݪݪݪݽͭLݭݺݭݭݭݭݫݮݭݪݭݭܽݭLDLڭݺګݽݭݭޮݻݭݽݭڽڭMMMݪLLLDLḒݫݭݭݽݭݭMڭݭݽݽڽݭڭݭݭ۽ݽݻMDLL۽̬ݽݭڽݺݽݽݽݻۺڽLݽͬ۽ݭݽݻݻݽݽݽݽDLDDDMLܼܪLMݻݻݽݫުݺګڪۻۭݻݽۭݽݻݭڻڻDDDL @ݭ нݽݽݽݭۭDDMMD лݽۻݺݫݻDDMMLDD 33333333333333033 3333333033333333333333033033333333ۻ۽ݭ۽LMMLD 33333333333333033 3333333033333333333333033033333333ݽۻݽM 33333333333333033 3333333033333333333333033033333333ݽ۽ݽݻMݽݻ 0333333033 333303333330330333333ݻMݻԴݻ 0333333033 333303333330330333333۽ۻ۽ݽۻݻݻMLݼ0333333033н 333303333330333333ܽ۽ݽ۽۽۽ݽ0333333033л 333303333330333333ۻݽ۽ܽ۽ݽ۽0333333033 333303333330333333ݻ۽ݻݽ۽033 3333333033 333303333333333303333333333ۻ۽ݽݽݽݽݽ033 3333333033 333303333333333303333333333ۻDLݽݻݻݻ033 3333033 33333330333333333303333 33ݽݻLLݽݻݻݻۻ۽033 3333033 3333333033333333330333333ۻ۽۽ݽݽ˽ݽݻܽۻ033 3333033н 33333330333333333303333 33ݽۻݻݽ033 3333033 3303333330330333333DݽMLLݽݵ033 3333033 3303333330330333333۽ݽ̽ݽLԻ۽033 3333033 330333333333333033033333333ͽDMܻݽMLL033 3333033 330333333333333033033333333ݽ\ML̼L0333333033 330333333333333033033333333LLL] ̼]LLL̽ ڞݽMMMMDLDL ~DLDLLDLLDDDLLDD DMDLDMLDDMDMLLݭݭLDMLDDDDLDDLݞڝCD~Dڞܮ~MDMLLDDDLJLLMMLޮLL|ުDMDDLĭLDṋM~ݝMMDLݮLLݭޭܭޝܮޮ~MḒJԭ ݭ|~DݪݭޮݭުN͞~LĮLy꭭ޮޝ=MLLywڪ =>D~Nݪڮު鮞ݞ鮮~y>>>wMޮww~>ꪪ~>yݭ~~wnw>}ݭ~=ݪݮwwwڪ~~~~>wwzڪ鞗p~~~=~~~}ݭ~w~yݚw~wy~~~~~~ݭyy~~w~ޮw~~ݙy~ww~>wꮮݮ~~y~ww~~ڮ~~~~~~w~~pw~~ڪ~~~~n~~~ywwwyy~~~wꮭz꭮~~w~~wnwwyywyyw~~~ꮧwz~~wy~w~wz~~}z~ywww痗w~y~w~ڮz~ڧ~~z~zmf~w~~y~wy~ww~隞~~ww~nmw~ywww~yy~~y~ꮮz瞚wnnw~~~z~w鞞ww癙w~~yy~zޗw~箧n~~wnwwwww痞wwpwwwwwꪮꪪ鞮~~w~zw~~ݮnwy~~wywxwy~~~~w~~w~f~~ww~y~~~~~y~w~~w~wwwy~~~~~nywy~www~wwww~y~~~~~~w~w~nv~~~~y~~~w~~wyw ~~ww~y~w~~~~w~ݮ~~~~yyw~www~w~~~~~~~w~~w~~wwyyw~~~yw~wwwwy~w~www~y~w~w~w~~~~~~~ww~~~yw瞞~w瞞wwww~ަvwwyy~w~~ywzwwy~~~瞞w~~yxwwwxwwy~~z~ww~~~ww~~ww~~~w~wyw~~wwwwy ww~~w~ww~w~~nzwyz~~~w~w~~~ww~y~~鞗y~y~y~~~~~w~~wwyw~~w~~w~w~yyy~yywwwp~wwww~wwywwwwyww~~www~~~~~~w~~~ww~~~w~~~w~www~ww~癗ww~~y~yyw~w~yw~~~~~ww~zwwypp~wwww~~w~~wyw~~~yw~w~y~~w~w~pwwwwwww~ywwwwy~~y~ywyywwwyw~~w鞗wy~w~~wwwwwwww~ywwww闗yw~ywwyy~ywwwwy~yw~y~w~wwwwwyyywwwwy~yyyywy~w~~~w~yw~~ywyww~w~y~wpnw~wwwyywywyy~痙ywyw~痗yyyywwy~wwwww~w~ywy~ꪪz箮ꪪ~pww~wwwz~wwywwww~w~wzwzw~wwwpwywww~~~w~wwwww~~wzzw~~~~~~ꪧ~wzwww~z鮮~z窪wꧪꪪ珏wxwwww~zwwzz窧~zxxwwww~~zzzw~箪ꮪꪪwwxwwwwwwwwww~ꮮꪮݪzݪ}wwwwwwwwwꮪݪꪪڪڪwwwwxww ~wwwwڪڪڭzﭪ}wꪪݭڪ~ڮޭڪڪzwzwyywwwyy~yywywڪݭݭڪݭݪݪwwwywwwywwwwywww~wݻڪڪڪݪڪޯwwww~~wwwwyywwy~wyw~~zꪪݽݽݭڪڪݽxw~w~www~wwwwwwwꮪݽݭݪݪڪݭww~pswww~wwwwݽݽݻݪݽwww~~~yݽݽݽݻݪݭ۫wwzy~wڪݺݻݽݽݽwwp?w~ﮭݭۭݽݪ~w~wwxwy3.zݽڪݽ۽ݽ۽ۭ۪wwww~3!}ݪڻݽݻݻ۪~ݭݽݪݭzywwww~ݪݭݭݪݭڭzwwwywwwxy~~~wwyxwݪڪݪڭݭݭ}wwwywwxywww/ڪڭݭݪڭݭڪݭ~wwwwwwwywywywp~ޭݪڪݭݭݪ~wwywwywywwwwyyp/ڭڪڪڭݪݪתzwww隗wwywwy~wwywy/ڪڪݪڭڪڭڪ~w~wyyywywww/ݪڪݪڭڭݭ}}wzzyywyw~>"ڭުݪڪݭݭzꪗ~ywwwwwޮ>ڭڭݪڪݪݭڪڪzwwwyw.ꮭڪݪڭڪڪzyywwwy>ﯪڪݪڪڪڪݪ~w~~>>ꮯݪݭڭݭݪުڪ}wy?/ꪪڪݪڪݪڪzzww"ݪڪꪮz~/3#ꪮꪪ~ 033033033333330330330333333333033 033333003330330333303303033000333003300333330 30033303030033030330003330033003333 303333033300333003300033300330333030333303300033033030303033/033300p03333300333333330 30033330003033303>p030003330030033330 30033003003030303000330033033030333~3033p033033003330333333333333 /".>>>">ꮮ3??~-- title: TriPeaks solitaire -- author: Potatopasty -- desc: TriPeaks solitaire -- site: https://potatopastysgames.neocities.org -- license: Creative Commons BY-NC 4.0 -- version: 0.1 -- script: lua -- input: mouse and controller ---------------------------------------- -------------- HELPERS ----------------- ---------------------------------------- function center_text(str, y, col, fixed, scale, small) -- draw off-screen purely to measure the rendered width local text_width = print(str, 0, -1000, col, fixed, scale, small) local x = (240 - text_width) // 2 print(str, x, y, col, fixed, scale, small) end function draw_prompt_box(x, y, w, h, bg_color, border_color, highlight_color) -- Background rect(x, y, w, h, bg_color) -- Calculate edges local right = x + w - 1 local bottom = y + h - 1 -- Top border / highlight line(x, y, right, y, border_color) line(x, y + 1, right - 1, y + 1, border_color) -- Left border / highlight line(x, y, x, bottom, border_color) line(x + 1, y, x + 1, bottom - 1, border_color) -- Bottom border line(x + 2, bottom - 1, right, bottom - 1, highlight_color) line(x + 1, bottom, right, bottom, highlight_color) -- Right border line(right - 1, y + 2, right - 1, bottom, highlight_color) line(right, y + 1, right, bottom, highlight_color) end function LZWDecompress(txt) function InitDict() -- init dictionary local dict = {} dict[0] = '0' dict[1] = '1' dict[2] = '2' dict[3] = '3' dict[4] = '4' dict[5] = '5' dict[6] = '6' dict[7] = '7' dict[8] = '8' dict[9] = '9' dict[10] = 'a' dict[11] = 'b' dict[12] = 'c' dict[13] = 'd' dict[14] = 'e' dict[15] = 'f' return 15, dict end -- start with decompression local dictIndx, dict = InitDict() -- process local out = '' local indxCode = 7 -- first 6 chars are image width and height local prevCode = -1 local currCode = -1 -- extract width and height of image local w = tonumber(string.sub(txt, 1, 3), 16) local h = tonumber(string.sub(txt, 4, 6), 16) prevCode = tonumber(string.sub(txt, indxCode, indxCode + 2), 16) indxCode = indxCode + 3 out = out .. dict[prevCode] while indxCode < #txt do currCode = tonumber(string.sub(txt, indxCode, indxCode + 2), 16) indxCode = indxCode + 3 if currCode == 4095 then -- flush dictionary dictIndx, dict = InitDict() prevCode = tonumber(string.sub(txt, indxCode, indxCode + 2), 16) indxCode = indxCode + 3 out = out .. dict[prevCode] elseif dict[currCode] ~= nil then out = out .. dict[currCode] dictIndx = dictIndx + 1 dict[dictIndx] = dict[prevCode] .. string.sub(dict[currCode], 1, 1) prevCode = currCode else dictIndx = dictIndx + 1 dict[dictIndx] = dict[prevCode] .. string.sub(dict[prevCode], 1, 1) out = out .. dict[currCode] prevCode = currCode end end -- is more efficient to manipulate table instead of strings local img = {} for i = 1, string.len(out) do img[i] = tonumber(string.sub(out, i, i), 16) end return img, w, h end -- compressed image data local comprImg = "0f008800d00a01001101301101201000d01601701401701801301601e01d01a01d01501e02501901f02502601402b01c02401f01a02a02a03002202101803601503503603703803502e00a03f03004004002f04204204102203204904402d04701b03703d01c02104504303e03901205304802305604305204f05c01b05f03205704d03a04c04e05a02606804802e05a01902406704f05d02004b03804e03f00c07803a06103107506902005f06a05405103f07606505007505905208807105c06304607300c06d06302706f08302902306008307207d02c08706b06005803507800a0780a50050a703b09903307a08407009305105707d05e0730940930540ad09509605d0af09804c0a503e0730ad06e0280b308a02908406808806102d03c03c0ac0690360040040860d40460cf09a05e0980c80cf06f0c805802c0b80b802f0b003408100d00b08f09c0e704500e00d0780050e107a0dc0530950f504501600b0740980f90e20c40ca0ae0ce0d409b0ab0410240f90c908e03a00c1070180e802600b0e811003b0e50e701d0e80860b41080d90b002b10710e00a00e1210a50fc01a10c04703903f12605710708204010e0b504c0890f91170270440f70eb0f212613706513a0650c21150f20380910910ac03612408411711700a11e0f814b0b108a10d03712a14e0dd1180b200e0a500c08014614a11c01514c03812410911f14e0dc0660d612b03b11f11307e0d612e14a0430e80a814b15401110b01114909212c1291511160de0c30cd17d12c0cc1680ab17813815112613d12107714315a01015d13407219011507d1620c811111e0db13301211211213f0fd15f06d0b711006913419f17411e0e80780040790431590d301c1111a106319207a12f0c002b12919f15215102e10e12f0b10141c004e1240ed1580790a31b20d416b0fc12e1781ce13317b17e1121460de1b61a505504b1491540391110eb0970f20121590bf1761e407113e1020d90620d505611b1621131ee1e80bb0151c700d0ed00c1ad0180791ea1fd1e01ed11813c14506c13505c12a08609103c0a51fe2010ee1441ac20b0181ad1e418f1ea10111620013e2171ee0ca1e20b01b614106500e0401221fb18e1f320c0ce08d21a0d81e919321c11c10a1ae18e1f91fc10c20410a1fa18e0d21fc1fb2131fc1ac21619321b10224417e1881ea15324201e1211f712015822600d2131461e821722a24919c21e17d08c10d1c103420a1442522110d323c1fc1870921b01f921323f26a14326b26c1940f923d1d60712072011442461cc1022762561210ed1f71f720b07925313113520c12a1a725a26719713e20716525924928719c0f926b26a26c21029721414320e15920c1402762261ca29e1fe2a021624d2801221c920f2152541a22a328d2540862072b32a51d52b21942af16210c2351ad29929a29a20e2950a528427b1fe2342aa2612af03a2a52cb01027e1f724d1f82c820e0132592b62af21b2b32b22d62d72022c52de0e72902e12882720ee2520d32102c323f2e70d226429d1fa2bd1431101a51901871072090a41fa2e72c323d2e22e129f15c1d614b18a12001127e0ed05c1f92c120e17e18717c0461b81d02201d212c1241f019e1252b125d11628819531b0502f411010b3222350912f12ed2ed26d0d400032c32d32e32f33033133232e1fa3330001e933633601800e33933d11233d32f03633614e33a0e734032e2b72cd1612e22ae31210b1ac3282122e703635029d03a3473583393423332f335735932d2ce35f33009100b35f11534301833a11035f2e42ff2e02181882d726423c2bd37325221229623c2b132d00337d37e37f38032f38038337f33238132e19c37c38438438638b38b38d38e37d39039233038739539139436019e1d22b114c1bd0eb35d30c37730a2622e62fa37832915938a39839932c3ac37e3930033411073ab3ac3b13b63313af3b13b93853bc3983411892cd18910c29325b1021b12c929e1ad0f93b53bf32e3af3ad3cf3bd32d10c3cd3913b73ce3973d93823d33d23b03be3d73883c42e314c3c42d73183c51022611b129f3e53693e03b23333963de3d10003f333735b3f63dd3cd3da3f533d3f73ae3fb3fa3df3fc3f04063882e63c636e28706523d32625d2643770041aa10a3bb3f24024033f53f711e4053dc40441d3584004193f13f442440141f42540732d1e718240b39c2543ee3c642f2db31b2bd1fb3654273fe42043b4233c03cc43942643c42233942240038744343c35a40940931411543139c18a3e31823ee39e3e600d4494494463fb2ff4594024484174284403fd3f145f43d3593003c34303c114c2363492933e82021d53b44624603fe45a42844e45d46146542a42941e39446547c3b331b18a3c136e2f723f2cc44e46e48615143b3ef4233e146332f1d547a41a3d041647f4414914183d83833312cd4723001d228226940a31a31d48d31848f45744049249a3c049044545e49847d44238e4213b843e4302f33a12392f130a3ec2e314025d4724704c525443b36b4744b04cd3d42e13d64b84b63403d047b3bd3f700e0094ad4d445b31d2dc3543252c120f46a48730141444e4554ac49649b41b10d4d138f4d33ff4d54ec4d73fb1c74b94784703e731f4be29011e3644c43a119e1442f74e843f46642b4db4b34d64783674ae4d249335f4f34f43df4d84904f250d13c31441419c26139c2c92c82bc40d2e62733544b245b50a4184773d135e5084f843a33650b49742844353244125126e3c840922744b4e300b51e1152652bd2b310b44c03600543852e00050753651139a52746151853047549b54a54a53552a45b5452e546c2c71462ab5004fe26d3273e42595452ff54c3fb4cb5565563f752d48249a4ba4eb54e47e4634f03415002540d23a33eb0792b651d0d12c226c3a730c4e34154cc32c5504764b44781444585314f35744b75515583db42b29b5250e759953b52429c0052622e929a53f2e852632b58833733c5a75aa3f729b58f5545725aa56d55955149c49f3301fb4e029c53d44b10a23240e2fc2e923e4e31c906534012136236201800500c5c603a1f65a95c85d15d23334db5c85b85a51232b658659f2611e44c05c42954c02a120b53f5d55d45e65d33331fc34030600903c5d05e95f15d25cc5d255f3525a45d83775b84c21b05df0a500f2bc2992535980043401f75f244a0915c64d933b33b0076086103625f45d153a23e23f32923a6182ee61b0ee37426f6003275fe2ea32807833a27f5f05b75e832e6133311442ed62832f2230366273075d362c36363732f6053335c75ea6253663782ec3503762412961c941161d64244b00460040d57b5423785631f93072d018c2ce2a765724d0916275bc0a421129c2613076621c703863427e24f6631c71f62d033b25052465b65a65b2a11f826f66366c01d1f82802c367a6740ed2e814315829764237532858261e56040d1f964d1ea59b22200966a24e01868f30524e66a00e0030d31f827f3532a90792a769e3056684d924c2cf2d000724f68f66230c2d10032806554da21223c69f3c969e4094d92232246b02cf00c18c00327f6b567a6bc2d000c0035c459d0d465f1fe26526361d00f0071c86cb32661a5e265512060d27e5ee2236626c26ae67c6bf5c424110a6636a83063071200d36a966727e6cf2d20042cf6966aa1212eb6c15fc5fa1f769324d0096b000d6d96ee65865a1ad65c3a46793eb6247066d12bc4e126a2326186bc00764f7106856852093086da03f63266230666865366c27f4c23a530a5c12d06d960c65666724f03b1216a90076fa65461d2a76904d94da2a72c430b33b57b6336a91206936d627f6a26332d267069b6fe5fb67e6d371232571270d74a69e00e00f0d13796cb68524e6946276e50de6b868f3a26585d769d64629d6b77326546a26560d373f6696b030a6547306a960d2847411446d07236ea6f033b00a6ec7312fb74269c75d22637465226f37570961d74b1ad60f00f6f277f3255de65524d7646aa69505c76f69e23d00e5642eb6e676166d6697423076fa72d72a7432a86aa72d6ad2d06f36822e66b82d16cf74078d6a569b24d6b269b7712e765861767f37960178200400a5df37a0d20006cf4363532ee3a474463224f6686f82806687aa1c86537962c765b29522672c7336966d579076300a68f6e96f10ee6ff6667407d065b78b6b25ee6686d66576591216ad63466d12007875c7446bc69027061d5397382e837a65e26d35000f7880066f25a02622eb7ca76b7ae5ce6da5d76ae29b2d27d06ee2a17736676d76f06bb66a78f6697b07b12d16ef6333565a077b00e7ad6587df7a17e41586aa1fa2cf3ec1c866d2f10655e15820a36742fc00d8010008010078012d44e27f36ea1f50177306781af6de69d7b45de73081b6557af6a56776da84c7a32817e27ab6775be2406bc6d67eb81d81b6ef2a77ef6a577169a6c35c469b52626978b1ac0db1e733c00f83a2002138517eb85c2d182966e7eb7f667166d8227e06fc84b66672e2a81206ec76475a5ce67c80b7318675386a07236d76a224f77827e86185084f7f65b887a29c88c5fb3277ff1ae3080107380110008820356f177566c07581c72822280d1f600c6cf8937566e585e18c6b884e6a26e92228818816336c85268b72ce79185e84d8167768978446776c95bd7842bf0ea0376ce74e81588a75685772687775463260d7ae7b58512cf73f79b8b684e87679e75a8768dc87f66b81f66b3ea8c37a37648e187c7e48458678988502817aa0d47c621475c01478880186e90185501284f6e687772684f6d76996bf7616e98b389c8c584b9108c56dd1f882779b2a88e93546b20136698ad81f8f38c66761fb75c68968d23b8316fc0070070865f080190000f0847c980c91e7162d17ea7a38827795cf8b78276f86767b587f7178736e38b39189167908c671d93d62e81b6a87ab7306be73d7db90a7ef6d78887c420877e85a4d992c6a708e00000a32c78877418c7288b794430592496c8bf8bc7ea7277a085f88c79d81a93e2cf7ef69366d7a68776be8a98bf6340d28246667ce6d87eb85381d8d97d989563381266121630672c7a192d66a86f96574e2d075596b99b30871a8e097098977573a6b796b7df1f581e6277a096b8286eb0096a78eb6636be9b28e28178e85cf65592c8808b78d47e287580c9998678db9062526f276a10293895f8248b296093083c96597e79b8080c27156e678f21785d91f22372476b85571980866368f9929b56a26be8e288791a8e565c6d87789c07279ba8f478d66591f9b471d4e08a16789678849927d792c9307887c280280384176793d8f17177646ae7b195f6a172a81a9549ba6696a79e279e7dd65c69276286127f9e56ec91a8a79a89838248bb829a0fa1a9249449c19c20ce8ed2222c99bb6a69ae95f9fd86f83a996930121788933a070309377c99998439d4a0aa201219af893829a0966a9a66a3a4a6636ec72d7eda3c9b66699d06f1a0b92c77776d8169547178f881c8e88832ce8428f389c07a7c9a157a195e9ae86f009a3333c6cf83a9ee6a59369078739809b4761a438f19dd8e47199daa436e99e1668992a5893d8609e588a91b7ac6eb8b29fa94c0ed6cf9856d797ea1f9219f885c9f08c77236cf6fa6ec9c972c9b9a6e83aa6f9657ca9688da6b899b9d22168278b69676ea9e86e99abaacaac9e892392294a634a9f8b3a1c93a7af8e975681da5b9268159c1857949a9ba9ea213079b9983a349017889926948839339bb95178d01aa409afab284e8c385e976ab2ab780babea849686628b2aca7e072e799a3fabc9f581e80b8ad93a69b7dda9ea9c6a99e1a43893ace8d3aca778a07a3c9d371d6a1816a118b9ad48f16f78df886adf6696dd8dca5e9f28df9caa9c85eabb9eab036ffae194c1ea8a89586fc7de732acbaf8a9dacd9fe8a5a377eeb21aad9bbab1ad6890ad47dba79a5dadeb0bb0cb2dac4a1da5ca43b14a4ca7784b73dabdac5abf90a75c8c2aaf9aea68ae7983a9d9fd332839b3cb3a9ab0c87ab9dfa799d69e894fab1a50b3484c94aa629dc1f7ae6ae56eb81b9148179808eca7bb43876808b609ca9ae79fa4db639fbae660f60f74e32e6ceacda9d9caa7396b691afe9699dc7778e57aea509a0b5bb65a10a167e3b14893a49b7ca42aee84d85fad0973696a0285f1f7006659b74b6fa30acb77892cb6fba2b4a5a99fe99683cb7c827acb7ca1c218c6a7940815b8385ba3d7d96e38ba72d0ad732970afd767ad6b99b3ab29892ba1a119b9767a8fa1f2d0a02010bc865c9dfb2f76ba3172dbd3a9ab9fa30732a3072b6ec9fe33cb27ac9b73b4ab479cabba6e6724777ac1223a8fbcfa3eaac9a0777885be96eb8919a867892eb3ab4aac8b22a2090f9b9948803b1a664994b9bb9b6a66eb00da12b91ab19fcac7ac795e90e993a9fb9e9ca9feb7683a9ceb63983b6faf8b90abab83c096956aaad3718addc1e7787e9a158b3b59b5fa7bb1300eb9b011c08a8ab3a5ee9cb03f6a77ce00dbffaeebff01cc2ec2e803a27a70baf8849e1ba47debdab1f6cf6cea9b72db739cea33b25964afb79eb29c49a31c37ab09519d6b7e9738849d5a81b477dd693b1fb52799a089589ad893a27b9cb90c32b83a9f27ebcb9e9654b986ef96ba56728c44c4779f90fb9ea31b7c6ce8b2bd4a9b74e60f964aa1c88a36c33ba0a67a7ec356a1a0c727bcd945b51c2aa0dacb5eec47985967aabb96afcbf7b980068030068bab12b49c19a206cfc75a607cec758e0bafae5af479f7a1bdac0eba06fac4baf7c7c9cb83cc8732d001964b4ebdbacb8d2af7ca96b7c37ab2a07c41b53799762ba38dfc319dac9bb816aaac8ab9c43b3dc3ec9f6da910cb7bf024db98c5cc3ec6bcdda9cc0ba9db9fa9ac82a686a9c81c0ec5600fb6f8a5c86996a3432c83cba383c9ce9fda8aba4ba0c90c20969aa8ccd0adaf6aa7983c41bc8940754b80934bd6b616dac64b459b5c08c1ebefb4ac3678e27fc3c7ab825cdba81b20a2195fc84cb3c45c45ba268fa36af7ac8c4b9fd6facbec14acf007008cfccfbcf9b4acf295eba1af7ab1884717c37b519e0b5badac96cd8b3cce7b62c3fc75b89c18bfa78faf1b8da21c4cc1ab74bfbc8eb12c52d57cf2a6b96592ccc1d5ec8ac4fd37cfbc85ce7c7fb29d4dbedb33ab2a1c9dc9ebba5c2dbf7d11d4e663caeb0cc42ba5307cab7d86f7d24c0da67c57ababd6c196eca6f72a008d33cfb6cea3586fb27aced61c8195fd33d23a37abaa42bacd44d48b2bd6a696cdeb21c2daf4d6aadec729b97a3ca2bc4946ca9b13cb2ba5c45b7cbfdc1b9c9bd6aa1d62a9eba7c519ffaf88a5c16a37c81d8ccfcca6a9ccefb47dac885bcfa7ed9ad9dae7da1d72ca6bf76d8be2b64730cd4b91bc4d2cdd3bd9b9fac9bf772bdcfdc5d5700ed89d94801a6bcbd839acddbccc132d901a34d8bdbfa32c70824d92d7abf8be2b29d73d9881ac29a3cda0c6dc8cd72df2b12893d11910950b20df7dcfbc4d727deb4bde0da2ba472acbccd74d9b789cd83bc4f34783bdb5e17d63d8c83bdc0cefbfadc2c82d97d9ab2bcd19cbb72b61e0bcb1b3bb72cdddccbf9e01c1bdf9c55afccfdbe1cc6947c7f6b7d2ac87d90d37de3c8632f001db800f008d89d8b008009e4adf0d5dba9d65b6ed57ceab4bd49aa4d082cfca4d7bb21a80c10d27b657cbe11bf0c93bd7da9c26ce7bef9fce14af5d73cc67dec4bb6ec49c478b2cb99939ae008a6f6ece4a72cd3595f7c2e44db827ee1fdf1de3e20e20d35c8adf37a0e3dd25e00cb6be9c22a27c579add57e04ad4d49cb1e2c9a3dc6d69dccdd6e66c47bc1af4e129cb9cdc56db3762b75cebdb7e476ce00874ee47009b24e1ceb1e83d36e89d36930e7ca9f8d2ba2df2df8db0c11dafc2ea8f9c9e3e88573ad74892768a7cd4a951b91d3bdcfca1cc7e32c1abf8c11cece50ca87a0cd0a37b26eacd8db7aa369cea69cf5de8b788a5997307930d5de43e21e189cdd58d56cbcbc1ea3e68bfdb71bfce91824c5ed75c91ed0ed07eadd2ed2ebfbfadf8a81a9ad53bd7bd9f0acf9d5febecebb70b22cf59fd992a33c88dece4e95fe49c51cbe32dd8bc4adf39ceebceafd64a31d8aebfe13ba0e10d74a13d82ec2ef6b50ccecd9ba1abaec6db2cc7be0ed8dd0dd0d49d69cb3cf9d5da34d53b49ebdc16e1ede8f18d92900de7eb4cf7e16e47d93f2192ce89e17ef1c85acfbd4ea9bfdc12e38e0bdc5be2f0dcbc764f0ec1be66ec0baaed5b71cece00daca78af5e13c8e788d5d8d2cc8a6b72aaf6a69eae86fd34e1cf24f59eb4c50ba7f20cf8deeb12e1fef0d36f16c4ae52bd9be1f0ec4df3ae239e2e04d29f3fddec70f0dd23c11ef6914b13bd7c9ff04d75ec2e0bacf902965a30997d5ec4ae1c900f86faee17faecf7903a36db8cfcef1901f16e20de6d92f22ed8d24c8ed28e55ac8ec4dabb63de0bd3f5bcb2fc4f63e8fc48e12c12a4dec4de0f9cbf88d3d65dc0f7e996b22f78d96e75d60e1fd89d31ebbe7edecf18c52965d8efead8cc13e39fbeba7f54fbed37db0e55d289fbc7e9cbf78af4f74f0e007fff00e01000e00701301000900900701700901201701001801301701302102101a00e01901a01f02601401201b01200702b01602e02503102b03100900f00e00f00f02e03a00700803a00f00003904103802001d03902700700004a03900703f04104f05004103903c04004a05604b04b04003902b01101303f03f03801103a03405f00802105305e03301003a01902801e01801700f06d01607107006705e03806706b02e07c01e01b01e06d01f07d02003001903402d01f07f03007c02d01502006f02e01c05407c05405305a09704c08801203f01e0550960420570a10530880000690a10a703803703701100609607909308806905f04c0a30ab01d08008204d08008f07201906c03607109c06d04c01605c01407008b08f0850450c702901c07f01b08607608b06f09201200808809506903e04d04e0520aa04c0ba03903e0a009f0e305504f0b30dc03b0210560e50a20000a906b0b40660660b401109b07703a0dc0b505c0f801000d0f81010ff01508201a0840b40d30dd02903100700d00a00a00e11011311111211611700a01311901011111511d1121131180f811d11503704d01b09c1290e609f0a50e009305e0530a70530080570980960ba0a60ee0df0a00ee1360b409512605b07b0b209f02107b0f811a02a0c708d11911902404808f0320b10c707e02a08811f11311a11011200715915d15d12401111015f11b0c715b16515d13706800f0fc04c03e16f0b51410dd0b113113a16e0081340e805a13b09e0520e91270960510221090c202207a08709c0e40d91090b111915a02d1150b10121150b918e01716303118e05d19015d12011101419f01d15c11b15b19901d12419d1a101006615f16b01317816e13f1310420e51430b40df0dd17c03d05813d1b913d09817e0df0f316b0530360e10b303c0e20790b20dd1c90610ab06a02e14d1a40241241aa1601a011402512018f16719e19315b0fe1da19111c19e1d611403711006300a06603816f0dd1cd0950e40dd0661320e61ac1cd0491bf0e81bc13c04104d0f017d0590aa0f312d1cf0e11f10361440601b209312a03b05d11a19c15c15c10d18e1631211e316411c1d214d1df21d16016711411f12016211611300f16303e00a20a0e31f116c03d04d1b02321f32001fa0551af0e703d1fe05004c13d1350590ed17b0a72010e01730b41c60eb0e222d0630681f41ae12703b14214814821711b1161d22141a51d908d1e62232212232611e115922725e22c1f816d24f20d1af13013803d0dc20316b0dc17820305824e1fc13c1371861fa0a100117614213f1f020b0e61f906926c1c622f2080221a712311e2662232251612901e721916700d29710f00a2971d30792982181000182081c40ea13824a0f616f2a813d0a91c712b17a2752781b413702516c09e23c0a224606c20820a13014007227a09d1f10e41c92bc14626519112125e11c12429e2222612991102cf10f2d222322900d17329a29a03a2281832c224e26f1b005417826c1382ad05f0670542442b10970e92b11f30252b71fc17e0e81a21091830af0652b409f0460652c202d0472bf28d2c919e2d32622621122cf11330829b3081141d805b2cf0d83071f12892bc2f92770e20fb23e2710f30f602103e1f61750f00ee18a2f01c10ae2b522615b2981201012d825e2982992991e029a26519300e10130b29733733a33428f22330a1142d33042932d830930929722928230e10d2e516a2321c912b1f031f0512410f320017923c2b61f62ec13a00e3540ee02b3280610320760c80850b818607102908a0232a13683452d034533029a37334137134734329b3743712d200f00b00d37f1f013710f1841c820d09f24a2f518a1272442ec2ab0fb1f327632138d13a32238d0970101350440b908136d0903690b904d39d10715411a30a33000b00b00c3303423703793702d837a3b016303700d2290eb09c2e73183132ad2821d025d0780311b42b706127c13b3543932403953942372401f001204a39e06e36836d17406c21003407d1a71a93463ac2973a93e037f2983e229e30b3462ce37f3e43e30ba3b613731d31534d2e51cf1731f92080b72370971cc2b413439616c13204b1783532440010170000013592ab0912f61030b708f40a27b0ba2ef25302129c2663423ac3ea4173773ae2d13782d13301c93ed3f12811331b20fa2e605b31816b09b1bf18003c3fc3941812450552742442054063c904006a2b314210310100341002d1081260a52511532ce3de4493df3e93e33de3e23a93b244941938000d2fd12942a3562ac1ae1cb38742a1c71f13c717c2ad43117b2b02eb1c004a27e05646a13509836105b25300333808d19d06f1190a300a0520ff22141f41800a3e94503df41844b48237a2cc3ae3ea44c00b1192dd3c203f1ac1450372330ab3513192e51cd39524309805e31f20224305149e2ec27923d1bc4a446e49433700325310030d0260381110aa0491260141e933737210f48b11044f4b944a45444b3e04543ea29a4534bf00b2f91271c31ce3824c710438f2c448f22e1c80ed4a535434f4320594d43941fb0e92f21fd4d423e38f47206b0100031231460244b20a32413b611c41d48b3ac47f48029b4ef3a84813734873744bd3481d01803821832a306309b2c31b13fb23b1702c226d49d3c62710202dd0df17c4da3c713f0a54621b34a603747a0394720034aa03700221c19f5014b03870ad0ba41c45447f30438047f5294f54824f63804bb30b4503464cd5360ff0961f84ce06250302248d31d2dd0954dc4912f91761fb1754301fd12a0a30e21b84b20a954736351c0100010024b52fa39904b0383d12072d63753393e53e14c14f64c03e34bb3e437f3051e101547b4e30ff1e440c53643b5371c336305d39239223c1793ff1c135c17a4230a92522ab38f27100f40712b38f58743d2b51b43c500f3e53e73b13ad4fa44a4c42cc4f02190900d43a52041234cd2a103c53807b1c603f0362ab4dd4322eb5493ca1fe2f11b342359d4705b20e55b038f0ae00d4724654e904006b3e43b14f92d03e64f941a5275bf4f75925bf15e3a03d201f06601c03307d3a223103d0170f62a30305d646d5782eb5115dd27f5855b30f236359d5505da0392983b52e847033b17a35e35e05337b37b3794862624f73395c341d41a37529a0821530bb3690850080860745ce53e31e31a0680203964de1f45492f160b2ae51013b5b634d5b655604f4070435385e10ff17f0e95e830737c3dd2d23e65c46256275c05c23e511204802240e5cb07439e0845d22ff0180eb0090522e32002e905749c1b05e635f2b723d2e551f5a45af4290e01cf0f251d0b64ec32746e33724053c3b55c44484b75945c74f83e555f1d615340e14e3a13654105a00840c113707351427664316c16f49966a35a2f00e60415565b05b24e30a355012e67867964d35e1c205262441b3313475c841d5c15c05c85c63441da10e5ca62e62c36640e62f5cf62e4d35af0d33fc5ae12c5114674da13e0960024386752ad0f35815862ea2044940fd2763635c93405f15f56af52a6285c662a1a115e1a50306b91063673670d50d507405240e57d3974dc5dd6701f624767800f5562895005af58303958a0960015e261451f0031c249411e3b25355906b05c233f68747d6b52982601e334741421415c6316310236921f101d20a3ca3206c40414302b557c00069958d5ef2075584e56144970e60100026a10ae5766d655b5b31e93b26211153a75f56826225fb6df11110f6211a31150161a802407f15d0c962f69517d1b350b63c4dd6f83262b120161200240767e5a612b70133a28464742b51e5a95b128006227126111732d1d500e4721e76dd5f771462937841e29537710341b21519d36439c15a3cd60963b6422af57c0ef3cb4d85855540ae6381b33370115b870269f0fd0b66742a31040e005212129702e32d11871870f3af3315f43406813dd3762631a429926001822c3d33a50bb46d24e6602af5b43593fd6c449f6a858a06b50f0967014e451d2105801cd6d800e6746d35eb5a22296870ab00a22910000e0015c12ca6e371341f74c3e75f659337237133b16328c19f2121a20137910961ec4de7855de5de4a13c376167247a5530f817167a3f55b351f7427056156a50b611822959d28e0035c13a76814517d932a7aa41b3e759e11b2d522111203c11e0cf4b24615466717bf6115ac1ba47a70b4ab5ef0b55874e57c77ce61b7cb76b5b665314722f0f25e72ca71811e7a229470f3416b23707b27dd71832b7e31e91670dd18e0db1345410e17be6413974d70565ab5430dd58a6737f939970467a5e246376749479379376570370c20471037406232c6dd59156a77c8032911ea26112729509922802d4d81455ab60f7e98178187bd1812b18217f28234cb0af6536a46a50f851c00351f6a30491b958801a2921428351e633e1da85f2282d47f17a24787e32fe57f20381c4a48456fa05767e7583c30df6d56533977268468438430f172735b87d0a18705de88005622324411088586f88143623263887e7be87a87a51101166a7b969f5848538530578976d904a89a89b00089a05689d8a18978a38a48a551c8998a689f8a08ab89c89e8ac1138ae8988a78af8b58b28aa8ac1be8ac8b789e8ab8b30558b34315116d56fc8711362f20428a88ae8cb8cc8cd8ad8ce8ca8cd8ca8d18d38b68cf8d78d88d58d289b8868d489b8de8b28be8ce8a38e41ff8d78da8cc8e08e34d951823d5498826f946587b13154f8e78df8d60408d18f604a6a18fa8ea8ff8d68fa13a8fe8f78eb0008dd8bd8e58cf89d9050a790240090b8e88b38d590d5dd1007ec4a523e1755af51204f9008e58f989b9218e535e8ca92590192890a8bb92387b8be8bd8bb3999148db8cb6d99148c192e89e03a8a98b69359098c18a269e81b0396a105602e27528966f7619388d68cf9273d194b89b94e0008fd9288b89400a19268cf0a090f8e59088e590c9338d838d8d13fe8e28a58d090490a20235f74205601a0e412d1757f227150f9068ea95292c8cf95404a95291124491f8cc97893a96998147998097e8e99099618ed9378fb8e18a996a92d1bc92255992d7f00b551516a9799588aa92c97995995993890a9908db8c192c83f8ca8c08aa94694b9619008bb8bb87b9608bc8b48de93705570b06104f1c387b67497138c54464f0a24986c56f487c97c88238d76986d8ec8ed75b0a78b19cc9d22e99c99d369c9b97f64c916a6fe82b9997ff3bd82c0e67067067057cc4e57c50114e57068507428568235183285bc57626312038773564f9df51c42e00f64c1cf85524d44079270438f79e6a97f22f428000f" -- image information, composed by the sequence of color indexes and the dimensions of the image local img, w, h = LZWDecompress(comprImg) -- to move in the screen local x = 0 local y = 0 function RenderImage(xofs, yofs, w, h, alpha) -- take the portion of the image starting by xofs, yofs that fits the screen -- w, h are the whole dimensions of the image for y = 0, 136 do -- screen height for x = 0, 240 do -- screen width if xofs + x >= 0 and yofs + y >= 0 and xofs + x < w and yofs + y < h then local ofs = (yofs + y) * w + (xofs + x) + 1 local n = img[ofs] if n ~= alpha then pix(x, y, n) end end end end end ---------------------------------------- ---------- CARD / DECK SETUP ----------- ---------------------------------------- -- card ranks (1=ace ... 13=king): local ranks = { "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" } -- card suits (1=spades, 2=clubs, 3=diamonds, 4=hearts), used as sprite ids: local suits = { "spades", "clubs", "diamonds", "hearts" } CARD_WIDTH = 16 CARD_HEIGHT = 24 local function create_deck() local d = {} for suit = 1, 4 do for rank = 1, 13 do table.insert(d, { rank = rank, suit = suit }) end end return d end -- fisher-yates shuffle local function shuffle_deck(d) for i = #d, 2, -1 do local j = math.random(i) d[i], d[j] = d[j], d[i] end end ---------------------------------------- ---------- TABLEAU LAYOUT --------------- ---------------------------------------- -- 28 tableau slots: 3 apex cards (1-3), 6 row2 cards (4-9), -- 9 row3 cards (10-18), 10 shared base-row cards (19-28). -- Standard tri-peaks layout: each upper card is "covered" by -- two cards in the row directly below it; the base row is -- always exposed. COVERED_BY[i] = {a, b}, or nil for base row. COVERED_BY = { [1] = { 4, 5 }, [2] = { 6, 7 }, [3] = { 8, 9 }, [4] = { 10, 11 }, [5] = { 11, 12 }, [6] = { 13, 14 }, [7] = { 14, 15 }, [8] = { 16, 17 }, [9] = { 17, 18 }, [10] = { 19, 20 }, [11] = { 20, 21 }, [12] = { 21, 22 }, [13] = { 22, 23 }, [14] = { 23, 24 }, [15] = { 24, 25 }, [16] = { 25, 26 }, [17] = { 26, 27 }, [18] = { 27, 28 }, } -- {HALFunits, depth} per tableau index. depth 0 = base row -- (bottom), depth 3 = apex (top). x position is derived from -- HALFunits so each card centers correctly over the two cards -- it covers -- these numbers fall out of the pairing above, -- they're not arbitrary. LAYOUT_DATA = { [1] = { 3, 3 }, [2] = { 9, 3 }, [3] = { 15, 3 }, [4] = { 2, 2 }, [5] = { 4, 2 }, [6] = { 8, 2 }, [7] = { 10, 2 }, [8] = { 14, 2 }, [9] = { 16, 2 }, [10] = { 1, 1 }, [11] = { 3, 1 }, [12] = { 5, 1 }, [13] = { 7, 1 }, [14] = { 9, 1 }, [15] = { 11, 1 }, [16] = { 13, 1 }, [17] = { 15, 1 }, [18] = { 17, 1 }, [19] = { 0, 0 }, [20] = { 2, 0 }, [21] = { 4, 0 }, [22] = { 6, 0 }, [23] = { 8, 0 }, [24] = { 10, 0 }, [25] = { 12, 0 }, [26] = { 14, 0 }, [27] = { 16, 0 }, [28] = { 18, 0 }, } local transparency = 7 --timer local start_time = 0 local elapsed_time = 0 local timer_running = false -- tableau MARGIN_X = 22 HALF = 10 BASE_Y = 70 ROW_Y = 14 STOCK_X, STOCK_Y = 8, 104 WASTE_X, WASTE_Y = 8 + CARD_WIDTH + 6, 104 TEXT_COLOR = 0 local mouse_prev_left = false local background_colors = { 1, 2, 3, 4, 6, 7, 8 } local background_color_index = 6 -- default background color is dark green (color 6) local deck_designs = { 48, 50, 52, 54, 56, 58, 60 } -- fish, castle... local deck_design_index = 2 -- default is fish image local parts = {} local winning_timer = 0 local fireworks_trigger = 0 local fireworks_timer = 0 ---------------------------------------- --------------- SCORING ----------------- ---------------------------------------- -- streak/peak-bonus scoring, hopefully matching the Microsoft -- Entertainment Pack TriPeaks rules (I only have GBC -- version): the nth consecutive tableau card played without a -- stock draw in between scores n points (1, 2, 3, 4...); -- drawing from stock resets the streak and costs a flat -- penalty; clearing a peak (its apex + row2 + row3 cards, 6 per -- peak, the shared base row doesn't count towards any one peak) -- pays a bonus -- 15 for each of the first two, 30 for the -- third and final one. STOCK_DRAW_PENALTY = 5 PEAK_BONUS_REGULAR = 15 PEAK_BONUS_FINAL = 30 -- each peak's own 6 cards (apex + its 2 row2 + its 3 row3 -- cards). these three groups don't overlap each other -- the -- overlap between peaks only happens at the shared base row, -- which is deliberately excluded from this grouping. PEAKS = { { 1, 4, 5, 10, 11, 12 }, { 2, 6, 7, 13, 14, 15 }, { 3, 8, 9, 16, 17, 18 }, } local score = 0 local streak = 0 local peak_cleared = { false, false, false } local peaks_cleared_count = 0 local game_state = "title" -- "title", "dealing", "playing", "won", "lost", "instructions" local quit_confirm = false -- true while the quit-to-title popup is showing local control_mode = "Mouse" -- "Mouse" or "Controller" -- chosen on the title screen local function tableau_x(i) return MARGIN_X + LAYOUT_DATA[i][1] * HALF end local function tableau_y(i) return BASE_Y - LAYOUT_DATA[i][2] * ROW_Y end -- draw order: base row first (so it's drawn "underneath"), -- apex last (drawn "on top"), matching the visual overlap. DRAW_ORDER = {} for i = 1, 3 do table.insert(DRAW_ORDER, i) end for i = 4, 9 do table.insert(DRAW_ORDER, i) end for i = 10, 18 do table.insert(DRAW_ORDER, i) end for i = 19, 28 do table.insert(DRAW_ORDER, i) end -- reverse of DRAW_ORDER: used for click detection, so a click -- checks apex/row2/row3 (the cards drawn on top) before base. local click_order = {} for i = #DRAW_ORDER, 1, -1 do table.insert(click_order, DRAW_ORDER[i]) end DEAL_ORDER = {} for i = 1, 3 do table.insert(DEAL_ORDER, i) end for i = 4, 9 do table.insert(DEAL_ORDER, i) end for i = 10, 18 do table.insert(DEAL_ORDER, i) end for i = 19, 28 do table.insert(DEAL_ORDER, i) end ---------------------------------------- ---------- STOCK / WASTE / TABLEAU ------ ---------------------------------------- local tableau = {} -- tableau[1..28] = card or nil (removed or not yet dealt) local stock = {} -- remaining 24 cards, drawn from the front local stock_pointer = 1 local waste = nil -- current top-of-waste card function check_peak_clear() for p, group in ipairs(PEAKS) do if not peak_cleared[p] then local all_gone = true for _, idx in ipairs(group) do if tableau[idx] ~= nil then all_gone = false break end end if all_gone then peak_cleared[p] = true peaks_cleared_count = peaks_cleared_count + 1 local bonus = peaks_cleared_count < 3 and PEAK_BONUS_REGULAR or PEAK_BONUS_FINAL score = score + bonus spawn_peak_bonus_popup(p, bonus) end end end end ---------------------------------------- ---------- DEALING ANIMATION ------------ ---------------------------------------- DEAL_START_X, DEAL_START_Y = 8, 150 -- off-screen, bottom DEAL_MOVE_FRAMES = 5 -- flight duration per card DEAL_DELAY_FRAMES = 5 -- gap between cards starting local dealing_queue = {} -- {index, card} entries still to be dealt local active_deal = nil -- the one card currently in flight, or nil local dealt = {} -- dealt[i] = true once a card has landed there local deal_delay_timer = 0 ---------------------------------------- --------- CARD FLIP ANIMATION ---------- ---------------------------------------- FLIP_DURATION = 10 local flip_animations = {} -- flip_animations[i] = { t = frame counter } ---------------------------------------- --------- MOVE-TO-WASTE ANIMATION ------ ---------------------------------------- MOVE_TO_WASTE_FRAMES = 8 local moving_card = nil -- {card, t, dur, sx, sy, ex, ey, x, y, face_down, flipping, flip_t} function is_exposed(i) if tableau[i] == nil then return false end local cb = COVERED_BY[i] if not cb then return true end local a_gone = dealt[cb[1]] and tableau[cb[1]] == nil local b_gone = dealt[cb[2]] and tableau[cb[2]] == nil return a_gone and b_gone end function start_flip(i) if tableau[i] then tableau[i].face_down = true flip_animations[i] = { t = 0 } end end function has_active_flip() return next(flip_animations) ~= nil end -- true while any animation is in progress. function is_busy_animating() return has_active_flip() or moving_card ~= nil end function start_newly_exposed_flips() for i = 1, 28 do if is_exposed(i) and tableau[i].face_down then start_flip(i) end end end function start_move_to_waste(i) moving_card = { card = tableau[i], t = 0, dur = MOVE_TO_WASTE_FRAMES, sx = tableau_x(i), sy = tableau_y(i), ex = WASTE_X, ey = WASTE_Y, x = tableau_x(i), y = tableau_y(i), face_down = false, flipping = false, flip_t = 0, } tableau[i] = nil streak = streak + 1 score = score + streak check_peak_clear() start_newly_exposed_flips() reposition_cursor_after_removal(i) end function start_move_to_waste_from_stock() moving_card = { card = stock[stock_pointer], t = 0, dur = MOVE_TO_WASTE_FRAMES, sx = STOCK_X, sy = STOCK_Y, ex = WASTE_X, ey = WASTE_Y, x = STOCK_X, y = STOCK_Y, face_down = true, flipping = false, flip_t = 0, flip_on_arrival = true, } end function update_move_to_waste() if not moving_card then return end local m = moving_card if m.flipping then -- The card has reached the waste position. Hold it there while -- the horizontal flip plays; reveal it in waste only afterwards. m.flip_t = m.flip_t + 1 if m.flip_t >= FLIP_DURATION then waste = m.card waste.face_down = false moving_card = nil end return end m.t = m.t + 1 local p = math.min(m.t / m.dur, 1) m.x = m.sx + (m.ex - m.sx) * p m.y = m.sy + (m.ey - m.sy) * p if p > 0.1 then spawn_card_particles(m.x + CARD_WIDTH / 2, m.y + CARD_HEIGHT / 2, m.card.suit, m.face_down) end if p >= 1 then if m.flip_on_arrival then -- The stock card is still face-down at the exact moment it lands. -- Start the flip from here, never during the flight. m.flipping = true m.flip_t = 0 else -- Tableau cards arrive already face-up. waste = m.card moving_card = nil end end end ---------------------------------------- --------- CARD TRAIL PARTICLES --------- ---------------------------------------- card_particles = {} CARD_BACK_COLOR = 9 -- this should change with card back design, but obviously it doesn't. function spawn_card_particles(x, y, suit, face_down) -- face-down cards can't reveal their suit color to the -- player -- use a fixed card-back tint instead. local color if face_down then color = CARD_BACK_COLOR else -- same red/black rule draw_card() already uses for rank text. color = (suit == 3 or suit == 4) and 2 or 0 end for i = 1, math.random(3, 5) do table.insert(card_particles, { x = x + math.random(-CARD_WIDTH // 2, CARD_WIDTH // 2), y = y + math.random(-CARD_HEIGHT // 2, CARD_HEIGHT // 2), dx = (math.random() * 2 - 1) * 0.5, dy = (math.random() * 2 - 1) * 0.5, life = math.random(10, 20), max_life = math.random(15, 25), color = color, size = math.random(1, 3), }) end end function update_card_particles() for i = #card_particles, 1, -1 do local p = card_particles[i] p.x = p.x + p.dx p.y = p.y + p.dy p.dy = p.dy + 0.02 -- slight gravity p.dx = p.dx * 0.98 p.dy = p.dy * 0.98 p.life = p.life - 1 if p.life <= 0 then table.remove(card_particles, i) end end end function draw_card_particles() for _, p in ipairs(card_particles) do local size = p.size * (p.life / p.max_life) if size >= 1 then circ(p.x, p.y, size, p.color) else pix(p.x, p.y, p.color) end end end ---------------------------------------- --------- PEAK BONUS POPUP ------------- ---------------------------------------- PEAK_BONUS_POPUP_FRAMES = 45 PEAK_BONUS_POPUP_RISE = 14 PEAK_BONUS_POPUP_COLOR = 0 local peak_bonus_popups = {} function spawn_peak_bonus_popup(peak_index, amount) local apex_index = PEAKS[peak_index][1] local text = "+" .. amount local text_width = print(text, 0, -1000, PEAK_BONUS_POPUP_COLOR) table.insert(peak_bonus_popups, { x = tableau_x(apex_index) + (CARD_WIDTH - text_width) / 2, y = tableau_y(apex_index), text = text, t = 0, }) end function update_peak_bonus_popups() for i = #peak_bonus_popups, 1, -1 do local p = peak_bonus_popups[i] p.t = p.t + 1 if p.t >= PEAK_BONUS_POPUP_FRAMES then table.remove(peak_bonus_popups, i) end end end function draw_peak_bonus_popups() for _, p in ipairs(peak_bonus_popups) do local y = p.y - PEAK_BONUS_POPUP_RISE * (p.t / PEAK_BONUS_POPUP_FRAMES) print(p.text, p.x, y, PEAK_BONUS_POPUP_COLOR) end end function deal_new_game() local deck = create_deck() shuffle_deck(deck) tableau = {} dealing_queue = {} for k = 1, 28 do local index = DEAL_ORDER[k] table.insert(dealing_queue, { index = index, card = deck[k] }) end active_deal = nil deal_delay_timer = 0 dealt = {} flip_animations = {} cursor_zone = "tableau" cursor_index = nil quit_confirm = false stock = {} for i = 29, #deck do table.insert(stock, deck[i]) end stock_pointer = 1 score = 0 streak = 0 peak_cleared = { false, false, false } peaks_cleared_count = 0 peak_bonus_popups = {} -- waste starts empty: the player's first move (either a -- tableau card or a stock draw) is what starts the pile. waste = nil start_time = 0 elapsed_time = 0 parts = {} winning_timer = 0 fireworks_trigger = 0 fireworks_timer = 0 game_state = "dealing" end -- advance the in-flight card, or start the next queued one. function update_dealing_animation() if active_deal then local a = active_deal a.t = a.t + 1 local p = math.min(a.t / a.dur, 1) -- The card is ALWAYS face-down while it is moving. a.x = a.sx + (a.ex - a.sx) * p a.y = a.sy + (a.ey - a.sy) * p if p > 0.1 then spawn_card_particles(a.x + CARD_WIDTH / 2, a.y + CARD_HEIGHT / 2, a.card.suit, true) end if p >= 1 then -- The card is stationary in the tableau before the flip starts. a.card.face_down = true tableau[a.index] = a.card dealt[a.index] = true active_deal = nil -- Base-row cards are exposed immediately, so flip them only now. if a.should_flip then start_flip(a.index) end end elseif has_active_flip() then -- Wait for a landing flip to finish before dealing the next card. return elseif #dealing_queue > 0 then deal_delay_timer = deal_delay_timer + 1 if deal_delay_timer >= DEAL_DELAY_FRAMES then deal_delay_timer = 0 local next_entry = table.remove(dealing_queue, 1) active_deal = { index = next_entry.index, card = next_entry.card, t = 0, dur = DEAL_MOVE_FRAMES, sx = DEAL_START_X, sy = DEAL_START_Y, ex = tableau_x(next_entry.index), ey = tableau_y(next_entry.index), x = DEAL_START_X, y = DEAL_START_Y, -- Every moving card starts and remains face-down. -- Only base-row cards flip after reaching their slot. should_flip = (COVERED_BY[next_entry.index] == nil), face_down = true, } end else -- queue empty, nothing in flight, and no landing flip: done. draw_from_stock(false) -- turn first card to waste; setup, not a player draw start_time = time() -- current time in seconds timer_running = true game_state = "playing" cursor_zone = "tableau" cursor_index = build_tableau_nav_list()[1] end end function update_flip_animations() for i, a in pairs(flip_animations) do a.t = a.t + 1 if a.t >= FLIP_DURATION then tableau[i].face_down = false flip_animations[i] = nil elseif a.t >= FLIP_DURATION // 2 then tableau[i].face_down = false end end end -- rank match with ace/king wrap allowed (12 = |13 - 1|). function ranks_match(a, b) local d = math.abs(a - b) return d == 1 or d == 12 end function try_play_tableau(i) if not is_exposed(i) then return false end -- an empty waste pile has nothing to match against, so any -- exposed card can start it. if waste == nil or ranks_match(tableau[i].rank, waste.rank) then start_move_to_waste(i) return true end return false end ---------------------------------------- ---------- BUTTON/CONTROLLER CURSOR ----- ---------------------------------------- cursor_index = nil function build_tableau_nav_list() local list = {} for i = 1, 28 do if is_exposed(i) then table.insert(list, i) end end table.sort(list, function(a, b) return tableau_x(a) < tableau_x(b) end) return list end function move_cursor_horizontal(delta) if cursor_zone ~= "tableau" then return end local list = build_tableau_nav_list() if #list == 0 then return end local pos = nil for p, i in ipairs(list) do if i == cursor_index then pos = p end end if pos then pos = ((pos - 1 + delta) % #list) + 1 else pos = 1 end cursor_index = list[pos] end function toggle_cursor_zone() if cursor_zone == "tableau" then cursor_zone = "stock" else cursor_zone = "tableau" local list = build_tableau_nav_list() local still_valid = false for _, i in ipairs(list) do if i == cursor_index then still_valid = true end end if not still_valid and #list > 0 then cursor_index = list[1] end end end function reposition_cursor_after_removal(removed_index) local newly_exposed = {} for i, cb in pairs(COVERED_BY) do if (cb[1] == removed_index or cb[2] == removed_index) and is_exposed(i) then table.insert(newly_exposed, i) end end if #newly_exposed > 0 then table.sort(newly_exposed, function(a, b) return math.abs(tableau_x(a) - tableau_x(removed_index)) < math.abs(tableau_x(b) - tableau_x(removed_index)) end) cursor_index = newly_exposed[1] return end local list = build_tableau_nav_list() if #list == 0 then return end -- tableau is empty -- about to win local removed_x = tableau_x(removed_index) for _, i in ipairs(list) do if tableau_x(i) > removed_x then cursor_index = i return end end for j = #list, 1, -1 do if tableau_x(list[j]) < removed_x then cursor_index = list[j] return end end cursor_index = list[1] -- shouldn't normally happen, but land somewhere valid end function confirm_cursor() if cursor_zone == "stock" then draw_from_stock() elseif cursor_zone == "tableau" and cursor_index then try_play_tableau(cursor_index) end end function draw_from_stock(charge_penalty) if charge_penalty == nil then charge_penalty = true end if stock_pointer <= #stock then start_move_to_waste_from_stock() stock_pointer = stock_pointer + 1 if charge_penalty then streak = 0 score = score - STOCK_DRAW_PENALTY end return true end return false end function tableau_empty() for i = 1, 28 do if tableau[i] ~= nil then return false end end return true end function has_valid_move() for i = 1, 28 do if is_exposed(i) then if waste == nil or ranks_match(tableau[i].rank, waste.rank) then return true end end end return false end function handle_won_lost() if not is_busy_animating() then if tableau_empty() and not is_busy_animating() then timer_running = false game_state = "won" elseif stock_pointer > #stock and not has_valid_move() then timer_running = false game_state = "lost" end end end ---------------------------------------- ---------- INPUT ------------------------- ---------------------------------------- function handle_mouse() local mx, my, left = mouse() if is_busy_animating() then mouse_prev_left = left return end if left and not mouse_prev_left then if mx >= STOCK_X and mx < STOCK_X + CARD_WIDTH and my >= STOCK_Y and my < STOCK_Y + CARD_HEIGHT then draw_from_stock() else for _, i in ipairs(click_order) do if is_exposed(i) then local cx, cy = tableau_x(i), tableau_y(i) if mx >= cx and mx < cx + CARD_WIDTH and my >= cy and my < cy + CARD_HEIGHT then try_play_tableau(i) break end end end end end mouse_prev_left = left end function draw_mouse_cursor() local mx, my, left = mouse() if left then spr(19, mx - 5, my, transparency, 1, false, false, 2, 2) else spr(17, mx - 5, my, transparency, 1, false, false, 2, 2) end end function handle_controller_title() if btnp(2) or btnp(3) then control_mode = control_mode == "Mouse" and "Controller" or "Mouse" elseif btnp(4) then deal_new_game() elseif btnp(7) then game_state = "instructions" end end function handle_controller_playing() if quit_confirm then -- swallow all other "playing" input while the popup is open if btnp(7) then quit_confirm = false -- cancel, resume the game elseif btnp(4) then quit_confirm = false game_state = "title" end return end if btnp(7) then quit_confirm = true return -- don't also fall through to anything below on this press end -- cosmetic hotkeys: available regardless of control_mode, same -- as they were before -- a mouse player can still use these. if btnp(6) then background_color_index = background_color_index + 1 if background_color_index > #background_colors then background_color_index = 1 end end if btnp(5) then deck_design_index = deck_design_index + 1 if deck_design_index > #deck_designs then deck_design_index = 1 end end if control_mode ~= "Controller" then return end if is_busy_animating() then return end if btnp(0) or btnp(1) then toggle_cursor_zone() elseif btnp(2) then move_cursor_horizontal(-1) elseif btnp(3) then move_cursor_horizontal(1) elseif btnp(4) then confirm_cursor() end end function handle_controller_lost_or_reset() if btnp(5) then game_state = "title" elseif btnp(4) then deal_new_game() end end function handle_controller_instructions() if btnp(7) then game_state = "title" end end function handle_controller() if game_state == "title" then handle_controller_title() elseif game_state == "playing" then handle_controller_playing() elseif game_state == "lost" or game_state == "gamereset" then handle_controller_lost_or_reset() elseif game_state == "instructions" then handle_controller_instructions() end end ---------------------------------------- --------------- TIMER ------------------ ---------------------------------------- function gametimer() -- if the timer is running, calculate the elapsed time if timer_running then elapsed_time = (time() - start_time) / 1000 end end function format_time(seconds) local minutes = math.floor(seconds / 60) local secs = math.floor(seconds % 60) -- add leading zeros if needed local minute_str = (minutes < 10) and "0" .. minutes or "" .. minutes local second_str = (secs < 10) and "0" .. secs or "" .. secs return minute_str .. ":" .. second_str -- return the formatted time end function draw_timer() print("Time: " .. format_time(elapsed_time), 1, 1, TEXT_COLOR) end function draw_score() print("Score: " .. score, 1, 8, TEXT_COLOR) end function draw_left_in_stock() print("Cards left: " .. (#stock - stock_pointer + 1), 1, 130, TEXT_COLOR) end function draw_quit_note() if control_mode == "Mouse" then print("S: Quit", 206, 1, TEXT_COLOR) else print("Y: Quit", 206, 1, TEXT_COLOR) end end ---------------------------------------- ---------- UPDATE FUNCTIONS ------------ ---------------------------------------- function update() update_card_particles() update_peak_bonus_popups() if game_state == "dealing" then update_dealing_animation() update_flip_animations() elseif game_state == "playing" then gametimer() update_flip_animations() update_move_to_waste() if control_mode == "Mouse" then handle_mouse() end handle_won_lost() elseif game_state == "won" then update_winning() update_particles() trigger_particles() elseif game_state == "instructions" then -- update_instructions() end handle_controller() end ---------------------------------------- ----------- DRAW FUNCTIONS ------------- ---------------------------------------- function draw_scaled_sprite(id, x, y, w, h, dw, dh) local sx = (id % 16) * 8 local sy = (id // 16) * 8 local x2 = x + dw local y2 = y + dh local u2 = sx + w local v2 = sy + h ttri(x, y, x2, y, x, y2, sx, sy, u2, sy, sx, v2, false, 6) ttri(x2, y, x2, y2, x, y2, u2, sy, u2, v2, sx, v2, false, 6) end function draw_card(x, y, rank, suit, face_down) local suit_x = x + CARD_WIDTH - 12 local suit_y = y + CARD_HEIGHT - 16 local symbol_x = x + CARD_WIDTH - 14 local symbol_y = y + CARD_HEIGHT - 12 if face_down then spr(deck_designs[deck_design_index], x, y, transparency, 1, 0, 0, 2, 3) else spr(5, x, y, transparency, 1, 0, 0, 2, 3) local color = (suit == 3 or suit == 4) and 2 or 0 print(ranks[rank], x + 3, y + 2, color, false, 1, false) spr(suit, suit_x, suit_y, transparency) end end function draw_flipping_card(x, y, card, progress) -- 0..0.5: face-down shrinking; 0.5..1: face-up growing. local width_progress local face_down if progress < 0.5 then width_progress = 1 - progress * 2 face_down = true else width_progress = (progress - 0.5) * 2 face_down = false end local dw = math.max(1, math.floor(CARD_WIDTH * width_progress + 0.5)) local dx = x + math.floor((CARD_WIDTH - dw) / 2) draw_scaled_sprite(face_down and 5 or 7, dx, y, 16, 24, dw, CARD_HEIGHT) if not face_down and dw >= 8 then local color = (card.suit == 3 or card.suit == 4) and 2 or 0 print(ranks[card.rank], dx + math.max(1, (dw - 10) // 2), y + 2, color) spr(card.suit, dx + math.max(0, dw - 12), y + CARD_HEIGHT - 16, transparency) end end function draw_dealing() if active_deal then draw_card(math.floor(active_deal.x), math.floor(active_deal.y), active_deal.card.rank, active_deal.card.suit, active_deal.face_down) end end function draw_tableau() for _, i in ipairs(DRAW_ORDER) do local card = tableau[i] if card then local flip = flip_animations[i] if flip then draw_flipping_card(tableau_x(i), tableau_y(i), card, flip.t / FLIP_DURATION) else draw_card(tableau_x(i), tableau_y(i), card.rank, card.suit, card.face_down) end end end end function draw_moving_card() if not moving_card then return end local m = moving_card local x = math.floor(m.x) local y = math.floor(m.y) if m.flipping then draw_flipping_card(x, y, m.card, m.flip_t / FLIP_DURATION) else draw_card(x, y, m.card.rank, m.card.suit, m.face_down) end end function draw_button_cursor() local x, y if cursor_zone == "stock" then x, y = STOCK_X, STOCK_Y elseif cursor_zone == "tableau" and cursor_index and tableau[cursor_index] then x, y = tableau_x(cursor_index), tableau_y(cursor_index) else return end if btn(4) then spr(19, x + 7, y + 18, transparency, 1, false, false, 2, 2) else spr(17, x + 7, y + 18, transparency, 1, false, false, 2, 2) end end function draw_stock_and_waste() if stock_pointer <= #stock then draw_card(STOCK_X, STOCK_Y, 1, 1, true) else -- draw empty place for stock spr(7, STOCK_X, STOCK_Y, transparency, 1, 0, 0, 2, 3) end if waste then draw_card(WASTE_X, WASTE_Y, waste.rank, waste.suit, false) else -- draw empty place for waste spr(7, WASTE_X, WASTE_Y, transparency, 1, 0, 0, 2, 3) end end function draw_title_screen() RenderImage(x, y, w, h, -1) local box_start_x = 58 local box_start_y = 66 local vox_width = 123 local box_height = 40 local box_bg_color = 14 local box_bright_highlight = 13 local box_dark_highlight = 15 draw_prompt_box(box_start_x, box_start_y, vox_width, box_height, box_bg_color, box_bright_highlight, box_dark_highlight) local mode_label = control_mode == "Mouse" and "Mouse" or "Controller" center_text("<- " .. mode_label .. " ->", 72, text_color, false, 1, false) if control_mode == "Mouse" then center_text("Press Z To Start!", 96, text_color, false, 1, false) center_text("S: Help", 81, text_color, false, 1, false) else center_text("Press A To Start!", 96, text_color, false, 1, false) center_text("Y: Help", 81, text_color, false, 1, false) end end function draw_instructions() local text_x = 10 center_text("INSTRUCTIONS", 2, text_color, false, 2, false) print("HOW TO PLAY", text_x, 20, text_color, false, 1, false) print("Play tableau cards onto the waste pile that are one rank", text_x, 30, text_color, false, 1, true) print("higher or lower (Ace and King count as neighbors too).", text_x, 37, text_color, false, 1, true) print("Cards under others must be uncovered first.", text_x, 44, text_color, false, 1, true) print("Stuck? Draw a new card from the stock.", text_x, 51, text_color, false, 1, true) print("SCORING", text_x, 62, text_color, false, 1, false) print("Each card in an unbroken streak scores more", text_x, 72, text_color, false, 1, true) print("than the last (1, 2, 3...). A stock draw resets the streak.", text_x, 79, text_color, false, 1, true) print("Clearing a peak pays a bonus. Aim for high score.", text_x, 86, text_color, false, 1, true) print("OTHER", text_x, 100, text_color, false, 1, false) print("You can change the background color and card design.", text_x, 110, text_color, false, 1, true) if control_mode == "Mouse" then center_text("S: Back To Title", 127, text_color, false, 1, false) else center_text("Y: Back To Title", 127, text_color, false, 1, false) end end function draw_game_reset() local box_start_x = 76 local box_start_y = 46 local vox_width = 87 local box_height = 40 local box_bg_color = 14 local box_bright_highlight = 13 local box_dark_highlight = 15 draw_prompt_box(box_start_x, box_start_y, vox_width, box_height, box_bg_color, box_bright_highlight, box_dark_highlight) if game_state == "gamereset" then center_text("Good Job!", 50, text_color, false, 1, false) else center_text("No More Moves!", 50, text_color, false, 1, false) end center_text("Score: " .. score, 60, text_color, false, 1, false) if control_mode == "Mouse" then center_text("Z: New Game", 69, text_color, false, 1, false) center_text("X: Title Screen", 78, text_color, false, 1, false) else center_text("A: New Game", 69, text_color, false, 1, false) center_text("B: Title Screen", 78, text_color, false, 1, false) end end function draw_quit_confirm() local box_start_x = 76 local box_start_y = 46 local box_width = 87 local box_height = 40 local box_bg_color = 14 local box_bright_highlight = 13 local box_dark_highlight = 15 draw_prompt_box(box_start_x, box_start_y, box_width, box_height, box_bg_color, box_bright_highlight, box_dark_highlight) center_text("Quit to title?", 50, text_color, false, 1, false) if control_mode == "Mouse" then center_text("Z: Quit", 69, text_color, false, 1, false) center_text("S: Cancel", 78, text_color, false, 1, false) else center_text("A: Quit", 69, text_color, false, 1, false) center_text("Y: Cancel", 78, text_color, false, 1, false) end end local function all(t) local i = 0 return function() i = i + 1 if t[i] then return t[i] end end end local function del(t, v) for i, val in ipairs(t) do if val == v then table.remove(t, i) return val end end end function update_winning() if winning_timer == 400 then -- stop triggering new particles fireworks_timer = fireworks_timer - 1 end if winning_timer == 500 then game_state = "gamereset" end winning_timer = winning_timer + 1 end local function draw_winning_text() local text = "CONGRATULATIONS!" local colors = { 2, 3, 4, 11, 5 } local base_x = 28 local base_y = 40 local elapsed_time = time() / 200 for i = 1, #text do local char = text:sub(i, i) local wave_offset = math.sin(elapsed_time + i * 0.2) * 20 local x = base_x + (i - 1) * 12 local y = base_y + wave_offset local color_index = math.floor((elapsed_time + i) * 4) % #colors + 1 local color = colors[color_index] print(char, x, y + 20, color, false, 2) end end ---------------------------------------- ------------- PARTICLES ---------------- ---------------------------------------- -- this function spawns particles at a given x, y location with a specific color local function spawn_particles(x, y, color) for i = 1, 30 do -- create 30 particles table.insert(parts, { x = x, y = y, -- particle's origin point dx = math.random() * 2 - 1, dy = math.random() * 2 - 1, -- direction rad = 1, --radius for circle life = 40, -- lifespan color = color or 2 -- default color to white if none given }) end end function update_particles() for p in all(parts) do p.x = p.x + p.dx * 2 p.y = p.y + p.dy * 2 p.life = p.life - 1 if p.life < 0 then del(parts, p) end end end function draw_particles() for p in all(parts) do circ(p.x, p.y, p.rad, p.color) end end function trigger_particles() if fireworks_timer >= 0 then if fireworks_trigger == 0 then for i = 1, 2 do spawn_particles(math.random(15, 220), math.random(120), math.random(1, 5)) end fireworks_trigger = math.random(25) -- reset timer for delay (adjust for speed) else fireworks_trigger = fireworks_trigger - 1 -- decrease timer end end end function draw_fireworks() draw_particles() draw_winning_text() end function draw() local mx, my = mouse() poke(0x3FFB, 0) -- hide the default mouse cursor cls(background_colors[background_color_index]) if game_state == "title" then draw_title_screen() if control_mode == "Controller" then draw_button_cursor() else draw_mouse_cursor() end elseif game_state == "dealing" then draw_tableau() draw_dealing() if control_mode == "Controller" then draw_button_cursor() else draw_mouse_cursor() end elseif game_state == "playing" then draw_timer() draw_score() draw_quit_note() draw_left_in_stock() draw_tableau() draw_stock_and_waste() draw_moving_card() if control_mode == "Controller" then draw_button_cursor() else draw_mouse_cursor() end if quit_confirm then draw_quit_confirm() end elseif game_state == "won" then draw_tableau() draw_stock_and_waste() draw_moving_card() draw_fireworks() elseif game_state == "lost" then draw_tableau() draw_stock_and_waste() draw_moving_card() draw_game_reset() elseif game_state == "gamereset" then draw_game_reset() if control_mode == "Controller" then draw_button_cursor() else draw_mouse_cursor() end elseif game_state == "instructions" then draw_instructions() end draw_card_particles() draw_peak_bonus_popups() end ---------------------------------------- ---------- TIC-80 CALLBACKS ------------ ---------------------------------------- function BOOT() end function TIC() update() draw() end