-- -- ## ##### ###### ###### ###### -- ## ## ## ## ## ## ## ### -- ## ## ## ## ## ## ### -- ## ## ## ###### ## ###### -- ## ## ## ## ## ## ### -- ## ## ## ## ## ## ### ## -- ##### ##### ## ## ###### ###### -- -- ComputerCraft port of Tetris -- by LDDestroier -- -- Supports wall kicking, holding, fast-dropping, -- and ghost pieces. -- -- TO-DO: -- + Add multiplayer -- + Add random color pulsation (for effect!) local scr_x, scr_y = term.getSize() local keysDown = {} local game = { p = {}, -- stores player information paused = false, -- whether or not game is paused canPause = true, -- if false, cannot pause game (such as in online multiplayer) inputDelay = 0.05, -- amount of time between each input control = { moveLeft = keys.left, moveRight = keys.right, moveDown = keys.down, rotateLeft = keys.z, rotateRight = keys.x, fastDrop = keys.up, hold = keys.leftShift, quit = keys.q }, timers = {}, timerNo = 1 } game.startTimer = function(duration) game.timers[game.timerNo] = duration game.timerNo = game.timerNo + 1 return game.timerNo - 1 end game.cancelTimer = function(tID) game.timers[tID or 0] = nil end local tableCopy tableCopy = function(tbl) local output = {} for k,v in pairs(tbl) do if type(v) == "table" then output[k] = tableCopy(v) else output[k] = v end end return output end -- sets up brown as colors.special, for palette swapping magic(k) local tColors = tableCopy(colors) tColors.white = 1 tColors.brown = nil -- brown is now white tColors.special = 4096 term.setPaletteColor(tColors.special, 0xf0f0f0) term.setPaletteColor(tColors.white, 0xf0f0f0) -- initializes and fixes up a board -- boards are 2D objects that can display perfectly square graphics local clearBoard = function(board, xpos, ypos, newXsize, newYsize, newBGcolor) board = board or {} board.x = board.x or xpos or 1 board.y = board.y or ypos or 1 board.xSize = board.xSize or newXsize or 10 board.ySize = board.ySize or newYsize or 24 board.BGcolor = board.BGcolor or newBGcolor or "f" for y = 1, board.ySize do board[y] = board[y] or {} for x = 1, board.xSize do -- explanation on each space: -- { -- boolean; if true, the space is solid -- string; the hex color of the space -- number; the countdown until the space is made non-solid (inactive if 0) -- number; the countdown until the space is colored to board.BGcolor (inactive if 0) -- } board[y][x] = board[y][x] or {false, board.BGcolor, 0, 0} end end return board end -- tetramino information -- don't tamper with this or I'll beat your ass so hard that war veterans would blush local minos = { [1] = { -- I-piece canRotate = true, shape = { " ", "3333", " ", " ", } }, [2] = { -- L-piece canRotate = true, shape = { " 1", "111", " ", } }, [3] = { -- J-piece canRotate = true, shape = { "b ", "bbb", " ", } }, [4] = { -- O-piece canRotate = true, shape = { "44", "44", } }, [5] = { -- T-piece canRotate = true, shape = { " a ", "aaa", " ", } }, [6] = { -- Z-piece canRotate = true, shape = { "ee ", " ee", " ", } }, [7] = { -- S-piece canRotate = true, shape = { " 55", "55 ", " ", } }, ["gameover"] = { -- special "mino" for game over canRotate = false, shape = { " ccc ccc c c ccccc ccc c c ccccc cccc", "c c c cc cc c c c c c c c c", "c c c cc cc c c c c c c c c", "c cc ccccc c c c cccc c c c c cccc cccc", "c c c c c c c c c c c c c c", "c c c c c c c c c c c c c c", "c c c c c c c c c c c c c c", " ccc c c c c ccccc ccc c ccccc c c", } } } -- converts blit colors to colors api, and back local to_colors, to_blit = { [' '] = 0, ['0'] = 1, ['1'] = 2, ['2'] = 4, ['3'] = 8, ['4'] = 16, ['5'] = 32, ['6'] = 64, ['7'] = 128, ['8'] = 256, ['9'] = 512, ['a'] = 1024, ['b'] = 2048, ['c'] = 4096, ['d'] = 8192, ['e'] = 16384, ['f'] = 32768, }, {} for k,v in pairs(to_colors) do to_blit[v] = k end -- checks if (x, y) is a valid space on the board local doesSpaceExist = function(board, x, y) return (x >= 1 and x <= board.xSize) and (y >= 1 and y <= board.ySize) end -- checks if (x, y) is being occupied by a tetramino (or if it's off-board) local isSpaceSolid = function(board, _x, _y) local x, y = math.floor(_x), math.floor(_y) if doesSpaceExist(board, x, y) then return board[y][x][1] else return true end end -- ticks down a space's timers, which can cause it to become non-solid or background-colored local ageSpace = function(board, _x, _y) local x, y = math.floor(_x), math.floor(_y) if doesSpaceExist(board, x, y) then -- make space non-solid if timer elapses if board[y][x][3] ~= 0 then board[y][x][3] = board[y][x][3] - 1 if board[y][x][3] == 0 then board[y][x][1] = false end end -- color space board.BGcolor if timer elapses if board[y][x][4] ~= 0 then board[y][x][4] = board[y][x][4] - 1 if board[y][x][4] == 0 then board[y][x][2] = board.BGcolor end end end end -- generates a "mino" object, which can be drawn and manipulated on a board local makeNewMino = function(minoType, board, x, y, replaceColor) local mino = tableCopy(minos[minoType]) if replaceColor then for yy = 1, #mino.shape do mino.shape[yy] = mino.shape[yy]:gsub("[^ ]", replaceColor) end end -- what color the ghost mino will be mino.ghostColor = 0x353535 mino.x = x mino.y = y mino.lockBreaks = 16 -- anti-infinite measure mino.waitingForLock = false mino.board = board mino.minoType = minoType -- checks to see if the mino is currently clipping with a solid board space (with the offset values) mino.checkCollision = function(xOffset, yOffset) local cx, cy for y = 1, #mino.shape do for x = 1, #mino.shape[y] do cx = mino.x + x + (xOffset or 0) cy = mino.y + y + (yOffset or 0) if mino.shape[y]:sub(x,x) ~= " " then if isSpaceSolid(mino.board, cx, cy) then return true end end end end return false end -- rotates a mino, and kicks it off a wall if need be mino.rotate = function(direction) local output = {} local oldShape = tableCopy(mino.shape) local origX, origY = mino.x, mino.y for y = 1, #mino.shape do output[y] = {} for x = 1, #mino.shape[y] do if direction == 1 then output[y][x] = mino.shape[#mino.shape - (x - 1)]:sub(y,y) elseif direction == -1 then output[y][x] = mino.shape[x]:sub(-y, -y) else error("invalid rotation direction (must be 1 or -1)") end end output[y] = table.concat(output[y]) end mino.shape = output -- try to kick off wall/floor if mino.checkCollision(0, 0) then -- try T-spin triple rotation if not mino.checkCollision(-direction, 2) then mino.y = mino.y + 2 mino.x = mino.x - direction return true end -- kick off floor for y = 1, math.floor(#mino.shape) do if not mino.checkCollision(0, -y) then mino.y = mino.y - y return true end end -- kick off right wall for x = 0, -math.floor(#mino.shape[1] / 2), -1 do if not mino.checkCollision(x, 0) then mino.x = mino.x + x return true end -- try diagonal-down if not mino.checkCollision(x, 1) then mino.x = mino.x + x mino.y = mino.y + 1 return true end end -- kick off left wall for x = 0, math.floor(#mino.shape[1] / 2) do if not mino.checkCollision(x, 0) then mino.x = mino.x + x return true end -- try diagonal-down if not mino.checkCollision(x, 1) then mino.x = mino.x + x mino.y = mino.y + 1 return true end end mino.shape = oldShape return false else return true end end -- draws a mino onto a board; you'll still need to render the board, though mino.draw = function(isSolid) for y = 1, #mino.shape do for x = 1, #mino.shape[y] do if mino.shape[y]:sub(x,x) ~= " " then if doesSpaceExist(mino.board, x + math.floor(mino.x), y + math.floor(mino.y)) then mino.board[y + math.floor(mino.y)][x + math.floor(mino.x)] = { isSolid or false, mino.shape[y]:sub(x,x), isSolid and 0 or 0, isSolid and 0 or 1 } end end end end end -- moves a mino, making sure not to clip with solid board spaces mino.move = function(x, y, doSlam) if not mino.checkCollision(x, y) then mino.x = mino.x + x mino.y = mino.y + y return true elseif doSlam then for sx = 0, x, math.abs(x) / x do if mino.checkCollision(sx, 0) then mino.x = mino.x + sx - math.abs(x) / x break end end for sy = 0, y, math.abs(y) / y do if mino.checkCollision(0, sy) then mino.y = mino.y + sy - math.abs(y) / y break end end else return false end end return mino end -- generates a random number, excluding those listed in the _psExclude table local pseudoRandom = function(randomPieces) if #randomPieces == 0 then for i = 1, #minos do randomPieces[i] = i end end local rand = math.random(1, #randomPieces) local num = randomPieces[rand] table.remove(randomPieces, rand) return num end -- initialize players local initializePlayers = function() game.p[1] = { board = clearBoard({}, 2, 2, 10, 24, "f"), holdBoard = clearBoard({}, 13, 14, 4, 3, "f"), queueBoard = clearBoard({}, 13, 2, 4, 14, "f"), randomPieces = {}, -- list of all minos for pseudo-random selection hold = 0, -- current piece being held canHold = true, -- whether or not player can hold (can't hold twice in a row) queue = {}, -- current queue of minos to use lines = 0, -- amount of lines cleared, "points" combo = 0, -- amount of consequative line clears lastLinesClear = 0, -- previous amount of simultaneous line clears (does not reset if miss) level = 1, -- level determines speed of mino drop fallSteps = 0.1, -- amount of spaces the mino will draw each drop } game.p[2] = { board = clearBoard({}, 18, 2, 10, 24, "f"), holdBoard = clearBoard({}, 29, 14, 4, 3, "f"), queueBoard = clearBoard({}, 29, 2, 4, 14, "f"), randomPieces = {}, hold = 0, canHold = true, queue = {}, lines = 0, combo = 0, lastLinesClear = 0, level = 1, fallSteps = 0.1, } -- generates the initial queue of minos per player for p = 1, #game.p do for i = 1, #minos do game.p[p].queue[i] = pseudoRandom(game.p[p].randomPieces) end end end -- actually renders a board to the screen local renderBoard = function(board, bx, by, doAgeSpaces, blankColor) local char, line local tY = board.y + (by or 0) for y = 1, board.ySize, 3 do line = {("\143"):rep(board.xSize),"",""} term.setCursorPos(board.x + (bx or 0), tY) for x = 1, board.xSize do line[2] = line[2] .. (blankColor or board[y][x][2]) if board[y + 1] then line[3] = line[3] .. (blankColor or board[y + 1][x][2]) else line[3] = line[3] .. board.BGcolor end end term.blit(line[1], line[2], line[3]) line = {("\131"):rep(board.xSize),"",""} term.setCursorPos(board.x + (bx or 0), tY + 1) for x = 1, board.xSize do if board[y + 2] then line[2] = line[2] .. (blankColor or board[y + 1][x][2]) line[3] = line[3] .. (blankColor or board[y + 2][x][2]) elseif board[y + 1] then line[2] = line[2] .. (blankColor or board[y + 1][x][2]) line[3] = line[3] .. board.BGcolor else line[2] = line[2] .. board.BGcolor line[3] = line[3] .. board.BGcolor end end term.blit(line[1], line[2], line[3]) tY = tY + 2 end if doAgeSpaces then for y = 1, board.ySize do for x = 1, board.xSize do ageSpace(board, x, y) end end end end -- checks if you've done the one thing in tetris that you need to be doing local checkIfLineCleared = function(board, y) for x = 1, board.xSize do if not board[y][x][1] then return false end end return true end -- draws the score of a player, and clears the space where the combo text is drawn local drawScore = function(player) term.setCursorPos(2, 18) term.setTextColor(tColors.white) term.write((" "):rep(16)) term.setCursorPos(2, 18) term.write("Lines: " .. player.lines) term.setCursorPos(2, 19) term.write((" "):rep(16)) end local drawLevel = function(player) term.setCursorPos(13, 17) term.write("Lv" .. player.level .. " ") end -- draws the player's simultaneous line clear after clearing one or more lines -- also tells the player's combo, which is nice local drawComboMessage = function(player, lines) term.setCursorPos(2, 18) term.setTextColor(tColors.white) term.write((" "):rep(16)) local msgs = { "SINGLE", "DOUBLE", "TRIPLE", "TETRIS" } term.setCursorPos(2, 18) if lines == player.lastLinesCleared then if lines == 3 then term.write("OH BABY A ") else term.write("ANOTHER ") end end term.write(msgs[lines]) if player.combo >= 2 then term.setCursorPos(2, 19) term.setTextColor(tColors.white) term.write((" "):rep(16)) term.setCursorPos(2, 19) term.write(player.combo .. "x COMBO") end end -- god damn it you've fucked up local gameOver = function(player) local mino = makeNewMino("gameover", player.board, 12, 3) local color = 0 for i = 1, 130 do if i % 2 == 0 then mino.x = mino.x - 1 end mino.draw() renderBoard(player.board, 0, 0, true) for i = 1, 20 do color = color + 0.01 term.setPaletteColor(4096, math.sin(color) / 2 + 0.5, math.sin(color) / 2, math.sin(color) / 2) end sleep(0.05) end return end -- initiates a game as a specific player (takes a number) local startGame = function(playerNumber) term.setBackgroundColor(tColors.gray) term.clear() initializePlayers() local mino, ghostMino local dropTimer, inputTimer, lockTimer, tickTimer local evt, board, player local clearedLines = {} player = game.p[playerNumber] board = player.board local draw = function(isSolid) term.setPaletteColor(4096, mino.ghostColor) ghostMino.x = mino.x ghostMino.y = mino.y ghostMino.move(0, board.ySize, true) ghostMino.draw(false) mino.draw(isSolid) renderBoard(board, 0, 0, true) end local currentMinoType local takeFromQueue = true term.setCursorPos(13, 13) term.write("HOLD") renderBoard(player.holdBoard, 0, 0, true) while true do player.level = math.ceil((1 + player.lines) / 10) player.fallSteps = 0.075 * (1.33 ^ player.level) drawLevel(player) if takeFromQueue then currentMinoType = player.queue[1] end mino = makeNewMino( currentMinoType, board, math.floor(board.xSize / 2) - 2, 0 ) ghostMino = makeNewMino( currentMinoType, board, math.floor(board.xSize / 2) - 2, 0, "c" ) if takeFromQueue then table.remove(player.queue, 1) table.insert(player.queue, pseudoRandom(player.randomPieces)) end -- draw queue for i = 1, math.min(#player.queue, 4) do local m = makeNewMino( player.queue[i], player.queueBoard, #minos[player.queue[i]].shape[1] == 2 and 1 or 0, 1 + (3 * (i - 1)) + (i > 1 and 2 or 0) ) m.draw() end renderBoard(player.queueBoard, 0, 0, true) -- draw held piece if player.hold ~= 0 then local m = makeNewMino( player.hold, player.holdBoard, #minos[player.hold].shape[1] == 2 and 1 or 0, 0 ) end takeFromQueue = true drawScore(player) -- check to see if you've lost if mino.checkCollision() then gameOver(player) return end draw() dropTimer = game.startTimer(0) inputTimer = game.startTimer(game.inputDelay) game.cancelTimer(lockTimer or 0) tickTimer = os.startTimer(0.05) -- drop a piece while true do evt = {os.pullEvent()} -- tick down internal game timer system if evt[1] == "timer" and evt[2] == tickTimer then --local delKeys = {} for k,v in pairs(game.timers) do game.timers[k] = v - 0.05 if v <= 0 then os.queueEvent("gameTimer", k) game.timers[k] = nil end end tickTimer = os.startTimer(0.05) end if player.paused then if evt[1] == "key" then if evt[2] == game.control.pause then game.paused = false end end else if evt[1] == "key" then if evt[2] == game.control.quit then return elseif evt[2] == game.control.pause then game.paused = true elseif evt[2] == game.control.rotateRight then if mino.rotate(1) then ghostMino.y = mino.y ghostMino.rotate(1) game.cancelTimer(lockTimer or 0) mino.waitingForLock = false draw() end elseif evt[2] == game.control.rotateLeft then if mino.rotate(-1) then ghostMino.y = mino.y ghostMino.rotate(-1) game.cancelTimer(lockTimer or 0) mino.waitingForLock = false draw() end end if evt[3] == false then if evt[2] == game.control.moveLeft then mino.move(-1, 0) game.cancelTimer(lockTimer or 0) mino.waitingForLock = false draw() game.cancelTimer(inputTimer or 0) inputTimer = game.startTimer(game.inputDelay) elseif evt[2] == game.control.moveRight then mino.move(1, 0) game.cancelTimer(lockTimer or 0) mino.waitingForLock = false draw() game.cancelTimer(inputTimer or 0) inputTimer = game.startTimer(game.inputDelay) elseif evt[2] == game.control.fastDrop then mino.move(0, board.ySize, true) draw(true) player.canHold = true break elseif evt[2] == game.control.hold then if player.canHold then if player.hold == 0 then takeFromQueue = true else takeFromQueue = false end player.hold, currentMinoType = currentMinoType, player.hold player.canHold = false makeNewMino( player.hold, player.holdBoard, #minos[player.hold].shape[1] == 2 and 1 or 0, 0 ).draw() renderBoard(player.holdBoard, 0, 0, true) break end end end elseif evt[1] == "gameTimer" then if evt[2] == inputTimer then inputTimer = game.startTimer(game.inputDelay) if not game.paused then if keysDown[game.control.moveLeft] == 2 then if mino.move(-1, 0) then game.cancelTimer(lockTimer or 0) mino.waitingForLock = false draw() end end if keysDown[game.control.moveRight] == 2 then if mino.move(1, 0) then game.cancelTimer(lockTimer or 0) mino.waitingForLock = false draw() end end if keysDown[game.control.moveDown] then game.cancelTimer(lockTimer or 0) mino.waitingForLock = false if mino.move(0, 1) then draw() else draw(true) break end end end elseif evt[2] == dropTimer then dropTimer = game.startTimer(0) if not game.paused then if mino.checkCollision(0, 1) then if mino.lockBreaks == 0 then draw(true) player.canHold = true break elseif not mino.waitingForLock then mino.lockBreaks = mino.lockBreaks - 1 lockTimer = game.startTimer(math.max(0.2 / player.fallSteps, 0.25)) mino.waitingForLock = true end else mino.move(0, player.fallSteps, true) draw() end end elseif evt[2] == lockTimer then if not game.paused then player.canHold = true draw(true) break end end end end end clearedLines = {} for y = 1, board.ySize do if checkIfLineCleared(board, y) then table.insert(clearedLines, y) end end if #clearedLines == 0 then if player.canHold then player.combo = 0 end else player.combo = player.combo + 1 player.lines = player.lines + #clearedLines drawComboMessage(player, #clearedLines) player.lastLinesCleared = #clearedLines for i = 1, 0, -0.12 do term.setPaletteColor(4096, i,i,i) for l = 1, #clearedLines do for x = 1, board.xSize do board[clearedLines[l]][x][2] = "c" end end renderBoard(board, 0, 0, true) sleep(0.05) end for i = #clearedLines, 1, -1 do table.remove(board, clearedLines[i]) end for i = 1, #clearedLines do table.insert(board, 1, false) end board = clearBoard(board) end end end -- records all key input local getInput = function() local evt local keyTimer = {} local timerKey = {} while true do evt = {os.pullEvent()} if evt[1] == "key" and evt[3] == false then keysDown[evt[2]] = 1 timerKey[evt[2]] = os.startTimer(0.2) keyTimer[timerKey[evt[2]]] = evt[2] elseif evt[1] == "timer" then if keysDown[keyTimer[evt[2]]] then keysDown[keyTimer[evt[2]]] = 2 end elseif evt[1] == "key_up" then keysDown[evt[2]] = nil os.cancelTimer(timerKey[evt[2]] or 0) keyTimer[timerKey[evt[2]] or 0] = nil timerKey[evt[2]] = nil end end end local main = function() startGame(1) end parallel.waitForAny(main, getInput) -- reset palette to back from whence it came for k,v in pairs(colors) do if type(v) == "number" then term.setPaletteColor(v, term.nativePaletteColor(v)) end end print(colors.white) term.setBackgroundColor(colors.black) term.setTextColor(colors.white) for i = 1, 5 do term.scroll(1) if i == 3 then term.setCursorPos(1, scr_y) term.write("Thanks for playing!") end sleep(0.05) end term.setCursorPos(1, scr_y)