nonogamer9

nono2048 3! (A 2048 Clone For ComputerCraft that will blow your mind!)

Jun 30th, 2025 (edited)
61
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 24.76 KB | Gaming | 0 0
  1. local gridSize = 4
  2. local function getOSVersion()
  3.   local v = os.version()
  4.   if v:find("1%.7") then return "1.7.10"
  5.   elseif v:find("1%.8") then return "1.8"
  6.   elseif v:find("1%.9") then return "1.9"
  7.   else return "tweaked" end
  8. end
  9. local OS_VER = getOSVersion()
  10. local isCC_Tweaked = (OS_VER == "tweaked" or OS_VER == "1.9")
  11. local isVanilla = (OS_VER == "1.8")
  12. local isLegacy = (OS_VER == "1.7.10")
  13.  
  14. local hasWget = http and http.checkURL and shell and shell.run
  15. local function wget(url, filename)
  16.   if not hasWget then return false end
  17.   if fs.exists(filename) then return true end
  18.   local ok, err = pcall(function()
  19.     shell.run("wget", url, filename)
  20.   end)
  21.   return fs.exists(filename)
  22. end
  23.  
  24. local function isMosaicCapable()
  25.   local v = os.version()
  26.   local major = tonumber(v:match("(%d+)%.%d+"))
  27.   return major and major >= 2 or v:find("1%.8") or v:find("1%.9")
  28. end
  29.  
  30. local mosaicCapable = isMosaicCapable()
  31. local isColor = term.isColor()
  32. local w, h = term.getSize()
  33.  
  34. local tileColors, plasmaColors, mode7Colors
  35. if isColor then
  36.   tileColors = {
  37.     [0]    = colors.gray,
  38.     [2]    = colors.white,
  39.     [4]    = colors.lightGray,
  40.     [8]    = colors.yellow,
  41.     [16]   = colors.orange,
  42.     [32]   = colors.red,
  43.     [64]   = colors.magenta,
  44.     [128]  = colors.purple,
  45.     [256]  = colors.blue,
  46.     [512]  = colors.cyan,
  47.     [1024] = colors.green,
  48.     [2048] = colors.lime,
  49.     [4096] = colors.brown,
  50.     [8192] = colors.pink,
  51.   }
  52.   plasmaColors = {
  53.     colors.black, colors.blue, colors.green, colors.cyan,
  54.     colors.red, colors.purple, colors.brown, colors.lightGray,
  55.     colors.gray, colors.lightBlue, colors.lime, colors.yellow,
  56.     colors.pink, colors.magenta, colors.orange, colors.white
  57.   }
  58.   mode7Colors = {colors.lightGray, colors.gray}
  59. else
  60.   tileColors = {
  61.     [0]    = colors.gray,
  62.     [2]    = colors.white,
  63.     [4]    = colors.gray,
  64.     [8]    = colors.white,
  65.     [16]   = colors.gray,
  66.     [32]   = colors.white,
  67.     [64]   = colors.gray,
  68.     [128]  = colors.white,
  69.     [256]  = colors.gray,
  70.     [512]  = colors.white,
  71.     [1024] = colors.gray,
  72.     [2048] = colors.white,
  73.     [4096] = colors.gray,
  74.     [8192] = colors.white,
  75.   }
  76.   plasmaColors = {colors.black, colors.gray, colors.white}
  77.   mode7Colors = {colors.white, colors.gray}
  78. end
  79.  
  80. -- Shadow color mapping for pretty shadows
  81. local tileShadowColors = {
  82.   [colors.white]      = colors.lightGray,
  83.   [colors.lightGray]  = colors.gray,
  84.   [colors.yellow]     = colors.orange,
  85.   [colors.orange]     = colors.red,
  86.   [colors.red]        = colors.brown,
  87.   [colors.magenta]    = colors.purple,
  88.   [colors.purple]     = colors.blue,
  89.   [colors.blue]       = colors.cyan,
  90.   [colors.cyan]       = colors.gray,
  91.   [colors.green]      = colors.lime,
  92.   [colors.lime]       = colors.green,
  93.   [colors.brown]      = colors.gray,
  94.   [colors.pink]       = colors.magenta,
  95.   [colors.gray]       = colors.black,
  96.   [colors.black]      = colors.black,
  97.   [colors.lightBlue]  = colors.blue,
  98. }
  99.  
  100. local UPPER = "\143"
  101. local LOWER = "\131"
  102. local FULL  = "\132"
  103. local SPACE = " "
  104.  
  105. local function mosaicPixelColor(val)
  106.   if isColor then
  107.     return plasmaColors[math.floor(val * (#plasmaColors-1)) + 1]
  108.   else
  109.     if val < 0.33 then return colors.black
  110.     elseif val < 0.66 then return colors.gray
  111.     else return colors.white end
  112.   end
  113. end
  114.  
  115. local function newBuffer()
  116.   local buf = {}
  117.   for y=1,h do
  118.     buf[y] = {}
  119.     for x=1,w do
  120.       buf[y][x] = {bg=colors.black, fg=colors.white, ch=" "}
  121.     end
  122.   end
  123.   return buf
  124. end
  125.  
  126. local function blitToTerm(buf)
  127.   for y=1,h do
  128.     local bgLine, fgLine, chLine = "", "", ""
  129.     for x=1,w do
  130.       local cell = buf[y][x]
  131.       bgLine = bgLine .. string.format("%x", math.log(cell.bg)/math.log(2))
  132.       fgLine = fgLine .. string.format("%x", math.log(cell.fg)/math.log(2))
  133.       chLine = chLine .. cell.ch
  134.     end
  135.     term.setCursorPos(1, y)
  136.     term.blit(chLine, fgLine, bgLine)
  137.   end
  138. end
  139.  
  140. local speaker = peripheral.find("speaker")
  141. local canPlayMusic = false
  142. local canPlaySound = false
  143.  
  144. if isCC_Tweaked or isVanilla or isLegacy then
  145.   if speaker then
  146.     canPlaySound = true
  147.     if isCC_Tweaked then
  148.       canPlayMusic = true
  149.     end
  150.   end
  151. end
  152.  
  153. local TITLE_URL = "https://files.catbox.moe/ji6xje.raw"
  154. local GAME_URL  = "https://files.catbox.moe/y2w7rv.raw"
  155. local TITLE_FILE = "nono2048_title.raw"
  156. local GAME_FILE  = "nono2048_game.raw"
  157.  
  158. local function playPCM(filename, inputSampleRate, stopFlag)
  159.   if not canPlayMusic then return end
  160.   while not stopFlag[1] do
  161.     local file = fs.open(filename, "rb")
  162.     if not file then break end
  163.     local chunkSize = 8192
  164.     local outputSampleRate = 48000
  165.     local ratio = inputSampleRate / outputSampleRate
  166.     local samplesPerChunk = math.floor(chunkSize / ratio)
  167.     local filePos = 0
  168.     local totalBytes = fs.getSize(filename)
  169.     local playedBytes = 0
  170.  
  171.     while not stopFlag[1] and playedBytes < totalBytes do
  172.       file.seek("set", filePos)
  173.       local rawChunk = {}
  174.       for i = 1, chunkSize do
  175.         local byte = file.read()
  176.         if not byte then break end
  177.         table.insert(rawChunk, byte - 128)
  178.         filePos = filePos + 1
  179.         playedBytes = playedBytes + 1
  180.       end
  181.       if #rawChunk == 0 then break end
  182.  
  183.       local chunk = {}
  184.       if inputSampleRate ~= outputSampleRate then
  185.         for i = 1, math.floor(#rawChunk / ratio) do
  186.           local src = (i - 1) * ratio + 1
  187.           local idx = math.floor(src)
  188.           local frac = src - idx
  189.           local s1 = rawChunk[idx] or 0
  190.           local s2 = rawChunk[idx + 1] or s1
  191.           table.insert(chunk, math.floor(s1 + (s2 - s1) * frac + 0.5))
  192.         end
  193.       else
  194.         chunk = rawChunk
  195.       end
  196.  
  197.       while not speaker.playAudio(chunk) do
  198.         os.pullEvent("speaker_audio_empty")
  199.       end
  200.       os.sleep(0)
  201.     end
  202.     file.close()
  203.   end
  204. end
  205.  
  206. -- Only use 1.12.2 noteblock sounds for CraftOS 1.8
  207. local vanillaInstruments = {"harp", "bass", "snare", "hat", "basedrum", "guitar", "flute", "bell", "chime", "xylophone"}
  208. local tweakedInstruments = {"harp", "bass", "bell", "chime", "flute", "guitar", "xylophone", "bit"}
  209.  
  210. local function playMergeSound(val)
  211.   if not canPlaySound then return end
  212.   local instruments = isVanilla and vanillaInstruments or tweakedInstruments
  213.   local idx = 1
  214.   if val then
  215.     idx = (math.log(val or 2)/math.log(2)) % #instruments + 1
  216.   end
  217.   local instrument = instruments[math.floor(idx)]
  218.   local logVal = val and math.log(val)/math.log(2) or 1
  219.   local pitch = 0.5 + math.min(2, logVal) / 8
  220.   if speaker.playNote then
  221.     speaker.playNote(instrument, 3, pitch)
  222.   elseif speaker.playSound then
  223.     speaker.playSound("block.note_block."..instrument, 3, 1)
  224.   end
  225. end
  226.  
  227. local musicStopFlag = {false}
  228. local function startMusic(filename, inputSampleRate)
  229.   musicStopFlag[1] = false
  230.   parallel.waitForAny(
  231.     function() playPCM(filename, inputSampleRate, musicStopFlag) end
  232.   )
  233. end
  234. local function stopMusic()
  235.   musicStopFlag[1] = true
  236.   os.queueEvent("music_stop")
  237. end
  238.  
  239. if isCC_Tweaked and canPlayMusic then
  240.   if hasWget then
  241.     wget(TITLE_URL, TITLE_FILE)
  242.     wget(GAME_URL, GAME_FILE)
  243.   else
  244.     if not fs.exists(TITLE_FILE) or not fs.exists(GAME_FILE) then
  245.     end
  246.   end
  247. end
  248.  
  249. local function drawMosaicBackground(buf, t, mode, isMode7)
  250.   for y=1,h do
  251.     for x=1,w do
  252.       local top, bottom
  253.       if isMode7 then
  254.         local function mode7Pixel(px, py)
  255.           local mosaicH = h*2
  256.           local cx, cy = w/2, mosaicH/2
  257.           local camHeight, horizon, scale = 8, mosaicH/2*0.6, 0.7
  258.           local dx, dy = (px - cx), (py - cy)
  259.           local perspective = camHeight / (dy + horizon)
  260.           local wx = dx * perspective * scale
  261.           local wy = perspective * scale
  262.           local angle = t
  263.           local cosA, sinA = math.cos(angle), math.sin(angle)
  264.           local u = 16 + wx * cosA - wy * sinA
  265.           local v = 16 + wx * sinA + wy * cosA
  266.           local tx = math.floor(u) % 32
  267.           local ty = math.floor(v) % 32
  268.           return ((math.floor(tx/4) + math.floor(ty/4)) % 2 == 0) and mode7Colors[1] or mode7Colors[2]
  269.         end
  270.         top = mode7Pixel(x, (y-1)*2+1)
  271.         bottom = mode7Pixel(x, (y-1)*2+2)
  272.       else
  273.         local function plasmaPixel(px, py)
  274.           if mode == 1 then
  275.             local v = math.sin(px/2 + t) + math.sin(py/1.5 + t/2) + math.sin((px+py)/2 + t/3)
  276.             v = (v + 3) / 6
  277.             return mosaicPixelColor(v)
  278.           elseif mode == 2 then
  279.             local v = math.sin(math.sqrt((px-13)^2 + (py-7)^2)/2 + t)
  280.             v = (v + 1) / 2
  281.             return mosaicPixelColor(v)
  282.           elseif mode == 3 then
  283.             local v = math.sin(px/2 + t) * math.cos(py/2 + t)
  284.             v = (v + 1) / 2
  285.             return mosaicPixelColor(v)
  286.           else
  287.             local v = math.sin((px + py)/2 + t) + math.cos((px - py)/2 - t)
  288.             v = (v + 2) / 4
  289.             return mosaicPixelColor(v)
  290.           end
  291.         end
  292.         top = plasmaPixel(x, (y-1)*2+1)
  293.         bottom = plasmaPixel(x, (y-1)*2+2)
  294.       end
  295.       if top == bottom then
  296.         buf[y][x] = {bg=top, fg=top, ch=FULL}
  297.       elseif top == colors.black and bottom == colors.black then
  298.         buf[y][x] = {bg=colors.black, fg=colors.black, ch=SPACE}
  299.       elseif top == colors.black then
  300.         buf[y][x] = {bg=bottom, fg=top, ch=LOWER}
  301.       elseif bottom == colors.black then
  302.         buf[y][x] = {bg=top, fg=bottom, ch=UPPER}
  303.       else
  304.         buf[y][x] = {bg=bottom, fg=top, ch=LOWER}
  305.       end
  306.     end
  307.   end
  308. end
  309.  
  310. local mode7_bg = {}
  311. for y=0,31 do
  312.   mode7_bg[y] = {}
  313.   for x=0,31 do
  314.     if ((math.floor(x/4) + math.floor(y/4)) % 2 == 0) then
  315.       mode7_bg[y][x] = mode7Colors[1]
  316.     else
  317.       mode7_bg[y][x] = mode7Colors[2]
  318.     end
  319.   end
  320. end
  321.  
  322. local function mode7Floor(x, y, angle)
  323.   local cx, cy = w/2, h/2
  324.   local camHeight = 8
  325.   local horizon = cy * 0.6
  326.   local scale = 0.7
  327.   local dx = (x - cx)
  328.   local dy = (y - cy)
  329.   local perspective = camHeight / (dy + horizon)
  330.   local wx = dx * perspective * scale
  331.   local wy = perspective * scale
  332.   local cosA, sinA = math.cos(angle), math.sin(angle)
  333.   local u = 16 + wx * cosA - wy * sinA
  334.   local v = 16 + wx * sinA + wy * cosA
  335.   local tx = math.floor(u) % 32
  336.   local ty = math.floor(v) % 32
  337.   return mode7_bg[ty][tx]
  338. end
  339.  
  340. local function drawTitleScreen(t)
  341.   local buf = newBuffer()
  342.   if mosaicCapable then
  343.     drawMosaicBackground(buf, t, 1, true)
  344.   else
  345.     for y=1,h do
  346.       for x=1,w do
  347.         buf[y][x].bg = mode7Floor(x, y, t)
  348.         buf[y][x].fg = colors.white
  349.         buf[y][x].ch = " "
  350.       end
  351.     end
  352.   end
  353.   local title = mosaicCapable and "nono2048 3! by nonogamer9! [ENHANCED MODE!]" or "nono2048 3! by nonogamer9!"
  354.   local subtitle = "Press any key to start"
  355.   local tx = math.floor((w - #title) / 2) + 1
  356.   local ty = math.floor(h / 2)
  357.   for i=1,#title do
  358.     buf[ty][tx+i-1].ch = title:sub(i,i)
  359.     buf[ty][tx+i-1].fg = isColor and colors.yellow or colors.white
  360.     buf[ty][tx+i-1].bg = colors.black
  361.   end
  362.   tx = math.floor((w - #subtitle) / 2) + 1
  363.   ty = ty + 2
  364.   for i=1,#subtitle do
  365.     buf[ty][tx+i-1].ch = subtitle:sub(i,i)
  366.     buf[ty][tx+i-1].fg = colors.white
  367.     buf[ty][tx+i-1].bg = colors.black
  368.   end
  369.   blitToTerm(buf)
  370. end
  371.  
  372. local function titleScreen()
  373.   local t = 0
  374.   local keyPressed = false
  375.   local function animate()
  376.     while not keyPressed do
  377.       drawTitleScreen(t)
  378.       t = t + 0.07
  379.       os.sleep(0.04)
  380.     end
  381.   end
  382.   local function input()
  383.     os.pullEvent("key")
  384.     keyPressed = true
  385.   end
  386.   if isCC_Tweaked and canPlayMusic and fs.exists(TITLE_FILE) then
  387.     parallel.waitForAny(
  388.       function() startMusic(TITLE_FILE, 32000) end,
  389.       animate,
  390.       input
  391.     )
  392.     stopMusic()
  393.   else
  394.     parallel.waitForAny(animate, input)
  395.   end
  396. end
  397.  
  398. local particles = {}
  399.  
  400. local function spawnParticles(x, y, count)
  401.   for i = 1, count do
  402.     local angle = math.random() * math.pi * 2
  403.     local speed = 0.2 + math.random() * 0.8
  404.     table.insert(particles, {
  405.       x = x + 0.5,
  406.       y = y + 0.5,
  407.       vx = math.cos(angle) * speed,
  408.       vy = math.sin(angle) * speed,
  409.       life = 10 + math.random(10),
  410.       ch = "*",
  411.       fg = colors.white,
  412.     })
  413.   end
  414. end
  415.  
  416. local function updateParticles()
  417.   local i = 1
  418.   while i <= #particles do
  419.     local p = particles[i]
  420.     p.x = p.x + p.vx
  421.     p.y = p.y + p.vy
  422.     p.life = p.life - 1
  423.     if p.life <= 0 then
  424.       table.remove(particles, i)
  425.     else
  426.       i = i + 1
  427.     end
  428.   end
  429. end
  430.  
  431. local function drawParticles(buf)
  432.   for _, p in ipairs(particles) do
  433.     local x, y = math.floor(p.x), math.floor(p.y)
  434.     if x >= 1 and x <= w and y >= 1 and y <= h then
  435.       buf[y][x].ch = p.ch
  436.       buf[y][x].fg = p.fg
  437.       buf[y][x].bg = colors.black
  438.     end
  439.   end
  440. end
  441.  
  442. local function drawLogo(buf, t)
  443.   local logo = "nono2048 3!"
  444.   local rainbow = {
  445.     colors.red, colors.orange, colors.yellow, colors.green,
  446.     colors.cyan, colors.blue, colors.purple, colors.pink
  447.   }
  448.   -- Calculate board height and position
  449.   local tileW, tileH = 4, 2
  450.   local gap = 1
  451.   local gridPixelH = gridSize * tileH + (gridSize-1)*gap
  452.   local startY_board = math.max(1, math.floor((h - gridPixelH)/2) - 4)
  453.   local boardBottom = startY_board + gridPixelH - 1
  454.  
  455.   -- Place logo 6 rows below the board, but not off the screen
  456.   local startY = math.min(h, boardBottom + 6)
  457.   local startX = math.floor((w - #logo) / 2) + 1
  458.  
  459.   for i = 1, #logo do
  460.     local ch = logo:sub(i, i)
  461.     local x = startX + i - 1
  462.     local y = startY + math.floor(math.sin(t * 2 + i * 0.5) * 1.5)
  463.     if y >= 1 and y <= h and x >= 1 and x <= w then
  464.       if isColor then
  465.         buf[y][x].ch = ch
  466.         buf[y][x].fg = rainbow[(i-1) % #rainbow + 1]
  467.         buf[y][x].bg = colors.black
  468.       else
  469.         buf[y][x].ch = ch
  470.         buf[y][x].fg = (i%2==0) and colors.gray or colors.white
  471.         buf[y][x].bg = colors.black
  472.       end
  473.     end
  474.   end
  475. end
  476.  
  477. local grid = {}
  478. local score = 0
  479. local currentPlasma = 1
  480. local seenDoubles = {}
  481. local mergedTi, mergedTj
  482.  
  483. local function getTileColor(val)
  484.   return tileColors[val] or (isColor and colors.white or colors.white)
  485. end
  486.  
  487. local function initGrid()
  488.   grid = {}
  489.   for i=1,gridSize do
  490.     grid[i] = {}
  491.     for j=1,gridSize do
  492.       grid[i][j] = 0
  493.     end
  494.   end
  495. end
  496.  
  497. local function spawnTile()
  498.   local empty = {}
  499.   for i=1,gridSize do
  500.     for j=1,gridSize do
  501.       if grid[i][j] == 0 then table.insert(empty, {i, j}) end
  502.     end
  503.   end
  504.   if #empty > 0 then
  505.     local pos = empty[math.random(#empty)]
  506.     grid[pos[1]][pos[2]] = (math.random() < 0.9) and 2 or 4
  507.   end
  508. end
  509.  
  510. local function canMove()
  511.   for i=1,gridSize do
  512.     for j=1,gridSize do
  513.       if grid[i][j] == 0 then return true end
  514.       if i < gridSize and grid[i][j] == grid[i+1][j] then return true end
  515.       if j < gridSize and grid[i][j] == grid[i][j+1] then return true end
  516.     end
  517.   end
  518.   return false
  519. end
  520.  
  521. local function move(dir)
  522.   local moved = false
  523.   local merged = false
  524.   local mergedValues = {}
  525.   local mergedGrid = {}
  526.   for i=1,gridSize do mergedGrid[i] = {}; for j=1,gridSize do mergedGrid[i][j] = false end end
  527.   local function traverse(start, finish, step)
  528.     local t = {}
  529.     for i=start,finish,step do table.insert(t, i) end
  530.     return t
  531.   end
  532.   local rows, cols
  533.   if dir == "up" then
  534.     rows, cols = traverse(1, gridSize, 1), traverse(1, gridSize, 1)
  535.   elseif dir == "down" then
  536.     rows, cols = traverse(gridSize, 1, -1), traverse(1, gridSize, 1)
  537.   elseif dir == "left" then
  538.     rows, cols = traverse(1, gridSize, 1), traverse(1, gridSize, 1)
  539.   elseif dir == "right" then
  540.     rows, cols = traverse(1, gridSize, 1), traverse(gridSize, 1, -1)
  541.   end
  542.   for _,i in ipairs(rows) do
  543.     for _,j in ipairs(cols) do
  544.       if grid[i][j] ~= 0 then
  545.         local ni, nj = i, j
  546.         while true do
  547.           local ti, tj = ni, nj
  548.           if dir == "up" then ti = ni - 1 end
  549.           if dir == "down" then ti = ni + 1 end
  550.           if dir == "left" then tj = nj - 1 end
  551.           if dir == "right" then tj = nj + 1 end
  552.           if ti < 1 or ti > gridSize or tj < 1 or tj > gridSize then break end
  553.           if grid[ti][tj] == 0 then
  554.             grid[ti][tj] = grid[ni][nj]
  555.             grid[ni][nj] = 0
  556.             ni, nj = ti, tj
  557.             moved = true
  558.           elseif grid[ti][tj] == grid[ni][nj] and not mergedGrid[ti][tj] then
  559.             grid[ti][tj] = grid[ti][tj] * 2
  560.             score = score + grid[ti][tj]
  561.             grid[ni][nj] = 0
  562.             mergedGrid[ti][tj] = true
  563.             moved = true
  564.             merged = true
  565.             table.insert(mergedValues, grid[ti][tj])
  566.             mergedTi, mergedTj = ti, tj
  567.             break
  568.           else
  569.             break
  570.           end
  571.         end
  572.       end
  573.     end
  574.   end
  575.   return moved, merged, mergedValues
  576. end
  577.  
  578. local function drawPlasmaToBuffer(buf, t)
  579.   if mosaicCapable then
  580.     drawMosaicBackground(buf, t, currentPlasma, false)
  581.     return
  582.   end
  583.   local plasma
  584.   if isColor then
  585.     plasma = {
  586.       function(x, y, t)
  587.         local v = math.sin(x/2 + t) + math.sin(y/1.5 + t/2) + math.sin((x+y)/2 + t/3)
  588.         v = (v + 3) / 6
  589.         return plasmaColors[math.floor(v * (#plasmaColors-1)) + 1]
  590.       end,
  591.       function(x, y, t)
  592.         local cx, cy = 13, 7
  593.         local v = math.sin(math.sqrt((x-cx)^2 + (y-cy)^2)/2 + t)
  594.         v = (v + 1) / 2
  595.         return plasmaColors[math.floor(v * (#plasmaColors-1)) + 1]
  596.       end,
  597.       function(x, y, t)
  598.         local v = math.sin(x/2 + t) * math.cos(y/2 + t)
  599.         v = (v + 1) / 2
  600.         return plasmaColors[math.floor(v * (#plasmaColors-1)) + 1]
  601.       end,
  602.       function(x, y, t)
  603.         local v = math.sin((x + y)/2 + t) + math.cos((x - y)/2 - t)
  604.         v = (v + 2) / 4
  605.         return plasmaColors[math.floor(v * (#plasmaColors-1)) + 1]
  606.       end,
  607.     }
  608.   else
  609.     plasma = {
  610.       function(x, y, t)
  611.         local v = math.sin(x/2 + t) + math.sin(y/1.5 + t/2)
  612.         v = (v + 2) / 4
  613.         if v < 0.33 then return colors.black
  614.         elseif v < 0.66 then return colors.gray
  615.         else return colors.white end
  616.       end,
  617.       function(x, y, t)
  618.         local v = math.sin((x+y)/3 + t)
  619.         v = (v + 1) / 2
  620.         if v < 0.33 then return colors.black
  621.         elseif v < 0.66 then return colors.gray
  622.         else return colors.white end
  623.       end,
  624.       function(x, y, t)
  625.         local v = math.cos(x/3 + y/3 + t)
  626.         v = (v + 1) / 2
  627.         if v < 0.33 then return colors.black
  628.         elseif v < 0.66 then return colors.gray
  629.         else return colors.white end
  630.       end,
  631.     }
  632.   end
  633.   local func = plasma[currentPlasma]
  634.   for y=1,h do
  635.     for x=1,w do
  636.       buf[y][x].bg = func(x, y, t)
  637.       buf[y][x].ch = " "
  638.       buf[y][x].fg = colors.white
  639.     end
  640.   end
  641. end
  642.  
  643. local function drawGridToBuffer(buf)
  644.   local tileW, tileH = 4, 2     -- width, height of each tile
  645.   local gap = 1                 -- gap between tiles
  646.   local gridPixelW = gridSize * tileW + (gridSize-1)*gap
  647.   local gridPixelH = gridSize * tileH + (gridSize-1)*gap
  648.   local startX = math.floor((w - gridPixelW)/2) + 1
  649.   local startY = math.max(1, math.floor((h - gridPixelH)/2) - 2)
  650.  
  651.   for i=1,gridSize do
  652.     for j=1,gridSize do
  653.       local val = grid[i][j]
  654.       local bg = getTileColor(val)
  655.       local fg = colors.black
  656.       local s = val == 0 and "" or tostring(val)
  657.       local pad = math.floor((tileW - #s)/2)
  658.       local x = startX + (j-1)*(tileW+gap)
  659.       local y = startY + (i-1)*(tileH+gap)
  660.  
  661.       -- Draw shadow (only for CraftOS 1.8+)
  662.       if isVanilla or isCC_Tweaked then
  663.         local shadowColor = tileShadowColors[bg] or colors.gray
  664.         for dy=0,tileH-1 do
  665.           for dx=0,tileW-1 do
  666.             local sx, sy = x+dx+1, y+dy+1
  667.             if sx>=1 and sx<=w and sy>=1 and sy<=h then
  668.               buf[sy][sx].bg = shadowColor
  669.               buf[sy][sx].fg = shadowColor
  670.               buf[sy][sx].ch = " "
  671.             end
  672.           end
  673.         end
  674.       end
  675.  
  676.       -- Draw tile
  677.       for dy=0,tileH-1 do
  678.         for dx=0,tileW-1 do
  679.           local tx, ty = x+dx, y+dy
  680.           if tx>=1 and tx<=w and ty>=1 and ty<=h then
  681.             buf[ty][tx].bg = bg
  682.             buf[ty][tx].fg = fg
  683.             buf[ty][tx].ch = " "
  684.           end
  685.         end
  686.       end
  687.  
  688.       -- Draw value centered on tile
  689.       if val ~= 0 then
  690.         local tx = x + pad
  691.         local ty = y + math.floor(tileH/2)
  692.         for k=1,#s do
  693.           if tx+k-1>=1 and tx+k-1<=w and ty>=1 and ty<=h then
  694.             buf[ty][tx+k-1].ch = s:sub(k,k)
  695.             buf[ty][tx+k-1].fg = fg
  696.             buf[ty][tx+k-1].bg = bg
  697.           end
  698.         end
  699.       end
  700.     end
  701.   end
  702. end
  703.  
  704. local function drawUIToBuffer(buf, fps)
  705.   local scoreStr = ("Score: %d"):format(score)
  706.   for i=1,#scoreStr do
  707.     buf[2][i+1].bg = colors.black
  708.     buf[2][i+1].fg = isColor and colors.yellow or colors.white
  709.     buf[2][i+1].ch = scoreStr:sub(i,i)
  710.   end
  711.   local fpsStr = ("FPS: %d"):format(fps)
  712.   for i=1,#fpsStr do
  713.     buf[3][i+1].bg = colors.black
  714.     buf[3][i+1].fg = isColor and colors.lime or colors.white
  715.     buf[3][i+1].ch = fpsStr:sub(i,i)
  716.   end
  717. end
  718.  
  719. local function drawGameOverToBuffer(buf)
  720.   local msg = "Game Over! R to restart, Q to quit."
  721.   for i=1,#msg do
  722.     buf[5][i+1].bg = colors.black
  723.     buf[5][i+1].fg = isColor and colors.red or colors.white
  724.     buf[5][i+1].ch = msg:sub(i,i)
  725.   end
  726. end
  727.  
  728. local function pollInput(timeout)
  729.   local timer = os.startTimer(timeout)
  730.   while true do
  731.     local e, p = os.pullEvent()
  732.     if e == "key" then
  733.       if p == keys.up then return "up"
  734.       elseif p == keys.down then return "down"
  735.       elseif p == keys.left then return "left"
  736.       elseif p == keys.right then return "right"
  737.       elseif p == keys.r then return "restart"
  738.       elseif p == keys.q then return "quit"
  739.       end
  740.     elseif e == "timer" and p == timer then
  741.       return nil
  742.     end
  743.   end
  744. end
  745.  
  746. local function mainGameLoop()
  747.   local t = 0
  748.   local frames = 0
  749.   local fps = 0
  750.   local lastFpsTime = os.clock()
  751.   local gameOver = false
  752.   local startY = math.floor((h - gridSize*2) / 2)
  753.  
  754.   initGrid()
  755.   spawnTile()
  756.   spawnTile()
  757.  
  758.   while true do
  759.     local frameStart = os.clock()
  760.     local buf = newBuffer()
  761.     drawPlasmaToBuffer(buf, t)
  762.     drawGridToBuffer(buf)
  763.     updateParticles()
  764.     drawParticles(buf)
  765.     drawLogo(buf, t)
  766.     drawUIToBuffer(buf, fps)
  767.     if gameOver then drawGameOverToBuffer(buf) end
  768.     blitToTerm(buf)
  769.     t = t + 0.15
  770.  
  771.     if gameOver then
  772.       local action = pollInput(0.1)
  773.       if action == "restart" then
  774.         initGrid()
  775.         score = 0
  776.         spawnTile()
  777.         spawnTile()
  778.         t = 0
  779.         gameOver = false
  780.         seenDoubles = {}
  781.         particles = {}
  782.       elseif action == "quit" then
  783.         stopMusic()
  784.         term.setCursorPos(1, 1)
  785.         term.setBackgroundColor(colors.black)
  786.         term.clear()
  787.         term.setCursorPos(1, 1)
  788.         return
  789.       end
  790.     else
  791.       local action = pollInput(0.1)
  792.       local moved, merged, mergedValues = false, false, nil
  793.       if action == "up" or action == "down" or action == "left" or action == "right" then
  794.         moved, merged, mergedValues = move(action)
  795.         if moved then
  796.           spawnTile()
  797.           if merged and mergedValues then
  798.             for _, v in ipairs(mergedValues) do
  799.               if not seenDoubles[v] then
  800.                 seenDoubles[v] = true
  801.                 currentPlasma = currentPlasma % (isColor and 4 or 3) + 1
  802.                 break
  803.               end
  804.             end
  805.             local x = (mergedTj-1)*4 + math.floor((w - gridSize*4 - (gridSize-1)*1)/2) + 3
  806.             local y = (mergedTi-1)*2 + math.max(1, math.floor((h - gridSize*2 - (gridSize-1)*1)/2) - 2) + 1
  807.             spawnParticles(x, y, 10)
  808.             if canPlaySound then playMergeSound(mergedValues[1]) end
  809.           end
  810.           if not canMove() then
  811.             gameOver = true
  812.           end
  813.         end
  814.       elseif action == "restart" then
  815.         initGrid()
  816.         score = 0
  817.         spawnTile()
  818.         spawnTile()
  819.         t = 0
  820.         gameOver = false
  821.         seenDoubles = {}
  822.         particles = {}
  823.       elseif action == "quit" then
  824.         stopMusic()
  825.         term.setCursorPos(1, 1)
  826.         term.setBackgroundColor(colors.black)
  827.         term.clear()
  828.         term.setCursorPos(1, 1)
  829.         return
  830.       end
  831.     end
  832.  
  833.     frames = frames + 1
  834.     local now = os.clock()
  835.     if now - lastFpsTime >= 1 then
  836.       fps = frames
  837.       frames = 0
  838.       lastFpsTime = now
  839.     end
  840.  
  841.     local frameTime = os.clock() - frameStart
  842.     if frameTime < 0.05 then
  843.       os.sleep(0.05 - frameTime)
  844.     end
  845.   end
  846. end
  847.  
  848. math.randomseed(os.time())
  849. titleScreen()
  850.  
  851. if isCC_Tweaked and canPlayMusic and fs.exists(GAME_FILE) then
  852.   parallel.waitForAll(
  853.     function() startMusic(GAME_FILE, 32000) end,
  854.     mainGameLoop
  855.   )
  856.   stopMusic()
  857. else
  858.   mainGameLoop()
  859. end
  860.  
Add Comment
Please, Sign In to add comment