--[[ ,--, ,---.'| | | : ,---, ,-.----. ,---, .--.--. ,----, : : | .' .' `\ \ / \ ,`--.' | / / '. .' .' \ | ' : ,---.' \ ; : \ | : : | : /`. / ,----,' | ; ; ' | | .`\ | | | .\ : : | ' ; | |--` | : . ; ' | |__ : : | ' | . : |: | | : | | : ;_ ; |.' / | | :.'| | ' ' ; : | | \ : ' ' ; \ \ `. `----'/ ; ' : ; ' | ; . | | : . / | | | `----. \ / ; / | | ./ | | : | ' ; | | \ ' : ; __ \ \ | ; / /-, ; : ; ' : | / ; | | ;\ \ | | ' / /`--' / / / /.`| | ,/ | | '` ,/ : ' | \.' ' : | '--'. / ./__; : '---' ; : .' : : :-' ; |.' `--'---' | : .' | ,.' | |.' '---' ; | .' '---' `---' `---' LDRIS 2 (Work in Progress) Current features: + Legitimate SRS rotation! + Line clearing! Crazy! + 7bag randomization! + Decent fucking controls! + Ghost piece! + Piece holding! + Piece queue! It's even animated! To-do: + Add score, and let lineclears and piece dropping add to it + Add an actual menu, and not that shit LDRIS 1 had + Multiplayer, as well as an implementation of garbage + Cheese race mode + Change color palletes so that the ghost piece isn't the color of dirt + Add in-game menu for changing controls (some people can actually tolerate guideline) --]] local scr_x, scr_y = term.getSize() -- client config can be changed however you please local clientConfig = { controls = { rotate_left = keys.z, -- by left, I mean counter-clockwise rotate_right = keys.x, -- by right, I mean clockwise move_left = keys.left, move_right = keys.right, soft_drop = keys.down, hard_drop = keys.up, sonic_drop = keys.space, hold = keys.leftShift, pause = keys.p, restart = keys.r, open_chat = keys.t, quit = keys.q, }, soft_drop_multiplier = 4.0, -- (SDF) the factor in which soft dropping effects the gravity move_repeat_delay = 0.25, -- (DAS) amount of time you must be holding the movement keys for it to start repeatedly moving (seconds) move_repeat_interval = 0.05, -- (ARR) speed at which the pieces move when holding the movement keys (seconds per tick) appearance_delay = 0, -- (ARE) amount of seconds it will take for the next piece to arrive after the current one locks into place lock_delay = 0.5, -- (Lock Delay) amount of seconds it will take for a resting mino to lock into placed queue_length = 5, -- amount of pieces visible in the queue (limited by size of UI) } -- ideally, only clients with IDENTICAL game configs should face one another local gameConfig = { minos = {}, -- list of all the minos (pieces) that will spawn into the board kickTables = {}, -- list of all kick tables for pieces currentKickTable = "SRS", -- current kick table randomBag = "singlebag", -- current pseudorandom number generator -- "singlebag" = normal tetris guideline random -- "doublebag" = doubled bag size -- "random" = using math.random board_width = 10, -- width of play area board_height = 40, -- height of play area board_height_visible = 20, -- height of play area that will render on screen (anchored to bottom) spin_mode = 1, -- 1 = allows T-spins -- 2 = allows J/L-spins -- 3 = allows ALL SPINS! Similar to STUPID mode in tetr.io can_rotate = true, -- if false, will disallow ALL piece rotation (meme mode) startingGravity = 0.15, -- gravity per tick for minos lock_move_limit = 30, -- amount of moves a mino can do after descending below its lowest point yet traversed -- used as a method of preventing stalling -- set it to math.huge for infinite } local switch = function(check) return function(cases) if type(cases[check]) == "function" then return cases[check]() elseif type(cases["default"] == "function") then return cases["default"]() end end end -- current state of the game; can be used to perfectly recreate the current scene of a game -- that includes board and mino objects, bitch gameState = {} --[[ (later, I'll probably store mino data in a separate file) spinID: 1 = considered a "T" piece, can be spun 2 = considered a "J" or "L" piece, can be spun if that's allowed 3 = considered every other piece, can be spun if STUPID mode is on ]] do -- define minos gameConfig.minos[1] = { shape = { " ", "@@@@", " ", " ", }, spinID = 3, color = "3", name = "I", kickID = 2, } gameConfig.minos[2] = { shape = { " @ ", "@@@", " ", }, spinID = 1, color = "a", name = "I", kickID = 1, } gameConfig.minos[3] = { shape = { " @", "@@@", " ", }, spinID = 2, color = "1", name = "L", kickID = 1, } gameConfig.minos[4] = { shape = { "@ ", "@@@", " ", }, spinID = 2, color = "b", name = "J", kickID = 1, } gameConfig.minos[5] = { shape = { "@@", "@@", }, spinID = 3, color = "4", name = "O", kickID = 2, spawnOffsetX = 1, } gameConfig.minos[6] = { shape = { " @@", "@@ ", " ", }, spinID = 2, color = "5", name = "S", kickID = 1, } gameConfig.minos[7] = { shape = { "@@ ", " @@", " ", }, spinID = 2, color = "e", name = "Z", kickID = 1, } end do -- define SRS kick table gameConfig.kickTables["SRS"] = { [1] = {}, -- used on J, L, S, T, Z tetraminos [2] = {}, -- used on I tetraminos } local srs = gameConfig.kickTables["SRS"] srs[1] = { ["01"] = {{ 0, 0}, {-1, 0}, {-1, 1}, { 0,-2}, {-1,-2}}, ["10"] = {{ 0, 0}, { 1, 0}, { 1,-1}, { 0, 2}, { 1, 2}}, ["12"] = {{ 0, 0}, { 1, 0}, { 1,-1}, { 0, 2}, { 1, 2}}, ["21"] = {{ 0, 0}, {-1, 0}, {-1, 1}, { 0,-2}, {-1,-2}}, ["23"] = {{ 0, 0}, { 1, 0}, { 1, 1}, { 0,-2}, { 1,-2}}, ["32"] = {{ 0, 0}, {-1, 0}, {-1,-1}, { 0, 2}, {-1, 2}}, ["30"] = {{ 0, 0}, {-1, 0}, {-1,-1}, { 0, 2}, {-1, 2}}, ["03"] = {{ 0, 0}, { 1, 0}, { 1, 1}, { 0,-2}, { 1,-2}}, ["02"] = {{ 0, 0}, { 0, 1}, { 1, 1}, {-1, 1}, { 1, 0}, {-1, 0}}, ["13"] = {{ 0, 0}, { 1, 0}, { 1, 2}, { 1, 1}, { 0, 2}, { 0, 1}}, ["20"] = {{ 0, 0}, { 0,-1}, {-1,-1}, { 1,-1}, {-1, 0}, { 1, 0}}, ["31"] = {{ 0, 0}, {-1, 0}, {-1, 2}, {-1, 1}, { 0, 2}, { 0, 1}}, } srs[2] = { ["01"] = {{ 0, 0}, {-2, 0}, { 1, 0}, {-2,-1}, { 1, 2}}, ["10"] = {{ 0, 0}, { 2, 0}, {-1, 0}, { 2, 1}, {-1,-2}}, ["12"] = {{ 0, 0}, {-1, 0}, { 2, 0}, {-1, 2}, { 2,-1}}, ["21"] = {{ 0, 0}, { 1, 0}, {-2, 0}, { 1,-2}, {-2, 1}}, ["23"] = {{ 0, 0}, { 2, 0}, {-1, 0}, { 2, 1}, {-1,-2}}, ["32"] = {{ 0, 0}, {-2, 0}, { 1, 0}, {-2,-1}, { 1, 2}}, ["30"] = {{ 0, 0}, { 1, 0}, {-2, 0}, { 1,-2}, {-2, 1}}, ["03"] = {{ 0, 0}, {-1, 0}, { 2, 0}, {-1, 2}, { 2,-1}}, ["02"] = {{ 0, 0}}, ["13"] = {{ 0, 0}}, ["20"] = {{ 0, 0}}, ["31"] = {{ 0, 0}}, } end -- returns a number that's capped between 'min' and 'max', inclusively local function between(number, min, max) return math.min(math.max(number, min), max) end -- copies the contents of a table table.copy = function(tbl) local output = {} for k,v in pairs(tbl) do output[k] = type(v) == "table" and table.copy(v) or v end return output end local stringrep = string.rep -- generates a new board, on which polyominos can be placed and interact local makeNewBoard = function(x, y, width, height) local board = {} board.contents = {} board.height = height or gameConfig.board_height board.width = width or gameConfig.board_width board.x = x board.y = y board.blankColor = "7" -- color if no minos are in that spot board.transparentColor = "f" -- color if the board tries to render where there is no board board.garbageColor = "8" board.visibleHeight = height and math.floor(height / 2) or gameConfig.board_height_visible for y = 1, board.height do board.contents[y] = stringrep(board.blankColor, width) end board.Write = function(x, y, color) x = math.floor(x) y = math.floor(y) board.contents[y] = board.contents[y]:sub(1, x - 1) .. color .. board.contents[y]:sub(x + 1) end board.AddGarbage = function(amount) local changePercent = 00 -- higher the percent, the more likely it is that subsequent rows of garbage will have a different hole local holeX = math.random(1, board.width) for y = amount, board.height do board.contents[y - amount + 1] = board.contents[y] end for y = board.height, board.height - amount + 1, -1 do board.contents[y] = stringrep(board.garbageColor, holeX - 1) .. board.blankColor .. stringrep(board.garbageColor, board.width - holeX) if math.random(1, 100) <= changePercent then holeX = math.random(1, board.width) end end end board.Clear = function(color) color = color or board.blankColor for y = 1, board.height do board.contents[y] = stringrep(color, board.width) end end -- used for sending board data over the network board.serialize = function(includeInit) return textutils.serialize({ x = includeInit and board.x or nil, y = includeInit and board.y or nil, height = includeInit and board.height or nil, width = includeInit and board.width or nil, blankColor = includeInit and board.blankColor or nil, visibleHeight = board.visibleHeight or nil, contents = board.contents }) end board.Render = function(...) -- takes list of minos that it will render atop the board local charLine1 = stringrep("\131", board.width) local charLine2 = stringrep("\143", board.width) local transparentLine = stringrep(board.transparentColor, board.width) local colorLine1, colorLine2, colorLine3 local minoColor1, minoColor2, minoColor3 local minos = {...} local mino local tY = board.y + math.floor((board.height - board.visibleHeight) * (2 / 3)) - 2 for y = board.height, 1 + (board.height - board.visibleHeight), -3 do colorLine1, colorLine2, colorLine3 = "", "", "" for x = 1, board.width do minoColor1, minoColor2, minoColor3 = nil, nil, nil for i = 1, #minos do mino = minos[i] if mino.visible then if mino.CheckSolid(x, y - 0, true) then minoColor1 = mino.color end if mino.CheckSolid(x, y - 1, true) then minoColor2 = mino.color end if mino.CheckSolid(x, y - 2, true) then minoColor3 = mino.color end end end colorLine1 = colorLine1 .. (minoColor1 or ((board.contents[y - 0] and board.contents[y - 0]:sub(x, x)) or board.blankColor)) colorLine2 = colorLine2 .. (minoColor2 or ((board.contents[y - 1] and board.contents[y - 1]:sub(x, x)) or board.blankColor)) colorLine3 = colorLine3 .. (minoColor3 or ((board.contents[y - 2] and board.contents[y - 2]:sub(x, x)) or board.blankColor)) end if (y - 0) <= (board.height - board.visibleHeight) then colorLine1 = transparentLine end if (y - 1) <= (board.height - board.visibleHeight) then colorLine2 = transparentLine end if (y - 2) <= (board.height - board.visibleHeight) then colorLine3 = transparentLine end term.setCursorPos(board.x, board.y + tY) term.blit(charLine1, colorLine2, colorLine1) tY = tY - 1 term.setCursorPos(board.x, board.y + tY) term.blit(charLine2, colorLine3, colorLine2) tY = tY - 1 end end return board end local makeNewMino = function(minoID, board, xPos, yPos, oldeMino) local mino = oldeMino or {} if not gameConfig.minos[minoID] then error("tried to spawn mino with invalid ID '" .. tostring(minoID) .. "'") else mino.shape = gameConfig.minos[minoID].shape mino.spinID = gameConfig.minos[minoID].spinID mino.kickID = gameConfig.minos[minoID].kickID mino.color = gameConfig.minos[minoID].color mino.name = gameConfig.minos[minoID].name end mino.finished = false mino.active = true mino.spawnTimer = 0 mino.visible = true mino.height = #mino.shape mino.width = #mino.shape[1] mino.minoID = minoID mino.x = xPos mino.y = yPos mino.xFloat = 0 mino.yFloat = 0 mino.board = board mino.rotation = 0 mino.resting = false mino.lockTimer = 0 mino.movesLeft = gameConfig.lock_move_limit mino.yHighest = mino.y mino.serialize = function(includeInit) return textutils.serialize({ minoID = includeInit and mino.minoID or nil, rotation = mino.rotation, x = x, y = y, }) end -- takes absolute position (x, y) on board, and returns true if it exists within the bounds of the board local DoesSpotExist = function(x, y) return ( x >= 1 and x <= board.width and y >= 1 and y <= board.height ) end -- checks if the mino is colliding with solid objects on its board, shifted by xMod and/or yMod (default 0) -- if doNotCountBorder == true, the border of the board won't be considered as solid -- returns true if it IS colliding, and false if it is not mino.CheckCollision = function(xMod, yMod, doNotCountBorder, round) local cx, cy -- represents position on board round = round or math.floor for y = 1, mino.height do for x = 1, mino.width do cx = round(-1 + x + mino.x + xMod) cy = round(-1 + y + mino.y + yMod) if DoesSpotExist(cx, cy) then if mino.board.contents[cy]:sub(cx, cx) ~= mino.board.blankColor and mino.CheckSolid(x, y) then return true end elseif (not doNotCountBorder) and mino.CheckSolid(x, y) then return true end end end return false end -- checks whether or not the (x, y) position of the mino's shape is solid. mino.CheckSolid = function(x, y, relativeToBoard) if relativeToBoard then x = x - mino.x + 1 y = y - mino.y + 1 end x = math.floor(x) y = math.floor(y) if y >= 1 and y <= mino.height and x >= 1 and x <= mino.width then return mino.shape[y]:sub(x, x) ~= " " else return false end end -- direction = 1: clockwise -- direction = -1: counter-clockwise mino.Rotate = function(direction, expendLockMove) local oldShape = table.copy(mino.shape) local kickTable = gameConfig.kickTables[gameConfig.currentKickTable] local output = {} local success = false local newRotation = ((mino.rotation + direction + 1) % 4) - 1 local kickRotTranslate = { [-1] = "3", [ 0] = "0", [ 1] = "1", [ 2] = "2", } if mino.active then -- get the specific offset table for the type of rotation based on the mino type local kickX, kickY local kickRot = kickRotTranslate[mino.rotation] .. kickRotTranslate[newRotation] -- translate the mino piece for y = 1, mino.width do output[y] = "" for x = 1, mino.height do if direction == -1 then output[y] = output[y] .. oldShape[x]:sub(-y, -y) elseif direction == 1 then output[y] = oldShape[x]:sub(y, y) .. output[y] end end end mino.width, mino.height = mino.height, mino.width mino.shape = output -- it's time to do some floor and wall kicking if mino.CheckCollision(0, 0) then for i = 1, #kickTable[mino.kickID][kickRot] do kickX = kickTable[mino.kickID][kickRot][i][1] kickY = -kickTable[mino.kickID][kickRot][i][2] if not mino.Move(kickX, kickY, false) then success = true break end end else success = true end if success then mino.rotation = newRotation mino.height, mino.width = mino.width, mino.height else mino.shape = oldShape end if expendLockMove then mino.movesLeft = mino.movesLeft - 2 if mino.movesLeft <= 0 then if mino.CheckCollision(0, 1) then mino.finished = 1 end else mino.lockTimer = clientConfig.lock_delay end end end return success end mino.Move = function(x, y, doSlam, expendLockMove) local didSlam local didCollide = false local didMoveX = true local didMoveY = true local step, round if mino.active then if doSlam then mino.xFloat = mino.xFloat + x mino.yFloat = mino.yFloat + y -- handle Y position if y ~= 0 then step = y / math.abs(y) round = mino.yFloat > 0 and math.floor or math.ceil if mino.CheckCollision(0, step) then mino.yFloat = 0 didMoveY = false else for iy = step, round(mino.yFloat), step do if mino.CheckCollision(0, step) then didCollide = true mino.yFloat = 0 break else didMoveY = true mino.y = mino.y + step mino.yFloat = mino.yFloat - step end end end else didMoveY = false end -- handle x position if x ~= 0 then step = x / math.abs(x) round = mino.xFloat > 0 and math.floor or math.ceil if mino.CheckCollision(step, 0) then mino.xFloat = 0 didMoveX = false else for ix = step, round(mino.xFloat), step do if mino.CheckCollision(step, 0) then didCollide = true mino.xFloat = 0 break else didMoveX = true mino.x = mino.x + step mino.xFloat = mino.xFloat - step end end end else didMoveX = false end else if mino.CheckCollision(x, y) then didCollide = true didMoveX = false didMoveY = false else mino.x = mino.x + x mino.y = mino.y + y didCollide = false didMoveX = true didMoveY = true end end local yHighestDidChange = (mino.y > mino.yHighest) mino.yHighest = math.max(mino.yHighest, mino.y) if yHighestDidChange then mino.movesLeft = gameConfig.lock_move_limit end if expendLockMove then mino.movesLeft = mino.movesLeft - 1 if mino.movesLeft <= 0 then if mino.CheckCollision(0, 1) then mino.finished = 1 end else mino.lockTimer = clientConfig.lock_delay end end else didMoveX = false didMoveY = false end return didCollide, didMoveX, didMoveY, yHighestDidChange end -- writes the mino to the board mino.Write = function() if mino.active then for y = 1, mino.height do for x = 1, mino.width do if mino.CheckSolid(x, y, false) then mino.board.Write(x + mino.x - 1, y + mino.y - 1, mino.color) end end end end end return mino end local random_bag = {} local pseudoRandom = function() return switch(gameConfig.randomBag) { ["random"] = function() return math.random(1, #gameConfig.minos) end, ["singlebag"] = function() if #random_bag == 0 then for i = 1, #gameConfig.minos do if math.random(0, 1) == 0 then random_bag[#random_bag + 1] = i else table.insert(random_bag, 1, i) end end end local pick = math.random(1, #random_bag) local output = random_bag[pick] table.remove(random_bag, pick) return output end, ["doublebag"] = function() if #random_bag == 0 then for r = 1, 2 do for i = 1, #gameConfig.minos do if math.random(0, 1) == 0 then random_bag[#random_bag + 1] = i else table.insert(random_bag, 1, i) end end end end local pick = math.random(1, #random_bag) local output = random_bag[pick] table.remove(random_bag, pick) return output end } end local handleLineClears = function(gameState) local mino, board = gameState.mino, gameState.board -- get list of full lines local clearedLines = {lookup = {}} for y = 1, board.height do if not board.contents[y]:find(board.blankColor) then clearedLines[#clearedLines + 1] = y clearedLines.lookup[y] = true end end -- clear the lines, baby if #clearedLines > 0 then local newContents = {} local i = board.height for y = board.height, 1, -1 do if not clearedLines.lookup[y] then newContents[i] = board.contents[y] i = i - 1 end end for y = 1, #clearedLines do newContents[y] = stringrep(board.blankColor, board.width) end gameState.board.contents = newContents end return clearedLines end StartGame = function() gameState = { gravity = gameConfig.startingGravity, board = makeNewBoard(2, 2, gameConfig.board_width, gameConfig.board_height), score = 0, antiControlRepeat = {}, topOut = false, canHold = true, didHold = false, heldPiece = false, paused = false, queue = {}, queueMinos = {}, } gameState.holdBoard = makeNewBoard( gameState.board.x + gameState.board.width + 1, gameState.board.y + gameState.board.visibleHeight * (1/3), 4, 4 ) gameState.holdBoard.visibleHeight = 4 gameState.queueBoard = makeNewBoard( gameState.board.x + gameState.board.width + 1, gameState.board.y, 4, gameState.board.height - gameState.holdBoard.height - 8 ) -- fill the queue for i = 1, clientConfig.queue_length do gameState.queue[i] = pseudoRandom() gameState.queueMinos[i] = makeNewMino( gameState.queue[i], gameState.queueBoard, 1, i * 3 + 12 ) end gameState.queue.cyclePiece = function() local output = gameState.queue[1] table.remove(gameState.queue, 1) gameState.queue[#gameState.queue + 1] = pseudoRandom() return output end gameState.mino = {} local qmAnim = 0 local makeDefaultMino = function(gameState) local nextPiece if gameState.didHold then if gameState.heldPiece then nextPiece, gameState.heldPiece = gameState.heldPiece, gameState.mino.minoID else nextPiece, gameState.heldPiece = gameState.queue.cyclePiece(), gameState.mino.minoID end else nextPiece = gameState.queue.cyclePiece() end return makeNewMino( nextPiece, gameState.board, math.floor(gameState.board.width / 2 - 1) + (gameConfig.minos[nextPiece].spawnOffsetX or 0), math.floor(gameConfig.board_height_visible + 1) + (gameConfig.minos[nextPiece].spawnOffsetY or 0), gameState.mino ) end gameState.mino = makeDefaultMino(gameState) local mino, board = gameState.mino, gameState.board local holdBoard, queueBoard = gameState.holdBoard, gameState.queueBoard local ghostMino = makeNewMino(mino.minoID, gameState.board, mino.x, mino.y, {}) local keysDown = {} local tickDelay = 0.05 local render = function(drawOtherBoards) board.Render(ghostMino, mino) if drawOtherBoards then holdBoard.Render() queueBoard.Render(table.unpack(gameState.queueMinos)) end end local tick = function(gameState) local didCollide, didMoveX, didMoveY, yHighestDidChange = mino.Move(0, gameState.gravity, true) local doCheckStuff = false local doAnimateQueue = false local doMakeNewMino = false qmAnim = math.max(0, qmAnim - 0.8) -- position queue minos properly for i = 1, #gameState.queueMinos do gameState.queueMinos[i].y = (i * 3 + 12) + math.floor(qmAnim) end if not mino.finished then mino.resting = (not didMoveY) and mino.CheckCollision(0, 1) if yHighestDidChange then mino.movesLeft = gameConfig.lock_move_limit end if mino.resting then mino.lockTimer = mino.lockTimer - tickDelay if mino.lockTimer <= 0 then mino.finished = 1 end else mino.lockTimer = clientConfig.lock_delay end end gameState.mino.spawnTimer = math.max(0, gameState.mino.spawnTimer - tickDelay) if gameState.mino.spawnTimer == 0 then gameState.mino.active = true gameState.mino.visible = true ghostMino.active = true ghostMino.visible = true end if mino.finished then if mino.finished == 1 then -- piece will lock gameState.didHold = false gameState.canHold = true -- check for top-out due to placing a piece outside the visible area of its board if false then -- I'm doing that later else doAnimateQueue = true mino.Write() doMakeNewMino = true doCheckStuff = true end elseif mino.finished == 2 then -- piece will attempt hold if gameState.canHold then gameState.didHold = true gameState.canHold = false -- I would have used a ternary statement, but didn't if gameState.heldPiece then doAnimateQueue = false else doAnimateQueue = true end -- draw held piece gameState.holdBoard.Clear() makeNewMino( gameState.mino.minoID, gameState.holdBoard, 1, 2, {} ).Write() doMakeNewMino = true doCheckStuff = true else mino.finished = false end else error("I don't know how, but that polyomino's finished!") end if doMakeNewMino then gameState.mino = makeDefaultMino(gameState) ghostMino = makeNewMino(mino.minoID, gameState.board, mino.x, mino.y, {}) if (not gameState.didHold) and (clientConfig.appearance_delay > 0) then gameState.mino.spawnTimer = clientConfig.appearance_delay gameState.mino.active = false gameState.mino.visible = false ghostMino.active = false ghostMino.visible = false end end if doAnimateQueue then table.remove(gameState.queueMinos, 1) gameState.queueMinos[#gameState.queueMinos + 1] = makeNewMino( gameState.queue[clientConfig.queue_length], gameState.queueBoard, 1, (clientConfig.queue_length + 1) * 3 + 12 ) qmAnim = 3 end -- if the hold attempt fails (say, you already held a piece), it wouldn't do to check for a top-out or line clears if doCheckStuff then -- check for top-out due to obstructed mino upon entry -- attempt to move mino at most 2 spaces upwards before considering it fully topped out gameState.topOut = true for i = 0, 2 do if mino.CheckCollision(0, -1) then mino.y = mino.y - 1 else gameState.topOut = false break end end handleLineClears(gameState) end end -- debug info term.setCursorPos(2, scr_y - 1) term.clearLine() term.write("Position: (" .. mino.x .. ":" .. mino.xFloat .. ", " .. mino.y .. "), Subheight: " .. mino.yFloat .. " ") term.setCursorPos(2, scr_y - 0) term.clearLine() term.write("Moves: " .. mino.movesLeft .. ", TimeToLock: " .. mino.lockTimer .. " ") end local checkControl = function(controlName, repeatTime, repeatDelay) repeatDelay = repeatDelay or 1 if keysDown[clientConfig.controls[controlName]] then if not gameState.antiControlRepeat[controlName] then if repeatTime then return keysDown[clientConfig.controls[controlName]] == 1 or ( keysDown[clientConfig.controls[controlName]] >= (repeatTime * (1 / tickDelay)) and ( repeatDelay and ((keysDown[clientConfig.controls[controlName]] * tickDelay) % repeatDelay == 0) or true ) ) else return keysDown[clientConfig.controls[controlName]] == 1 end end else return false end end local controlTick = function(gameState, onlyFastActions) local dc, dmx, dmy -- did collide, did move X, did move Y local didSlowAction = false if (not gameState.paused) and gameState.mino.active then if not onlyFastActions then if checkControl("move_left", clientConfig.move_repeat_delay, clientConfig.move_repeat_interval) then mino.Move(-1, 0, true, true) didSlowAction = true gameState.antiControlRepeat["move_left"] = true end if checkControl("move_right", clientConfig.move_repeat_delay, clientConfig.move_repeat_interval) then mino.Move(1, 0, true, true) didSlowAction = true gameState.antiControlRepeat["move_right"] = true end if checkControl("soft_drop", 0) then mino.Move(0, gameState.gravity * clientConfig.soft_drop_multiplier, true, false) didSlowAction = true gameState.antiControlRepeat["soft_drop"] = true end if checkControl("hard_drop", false) then mino.Move(0, board.height, true, false) mino.finished = 1 didSlowAction = true gameState.antiControlRepeat["hard_drop"] = true end if checkControl("sonic_drop", false) then mino.Move(0, board.height, true, true) didSlowAction = true gameState.antiControlRepeat["sonic_drop"] = true end if checkControl("hold", false) then mino.finished = 2 gameState.antiControlRepeat["hold"] = true didSlowAction = true end if checkControl("quit", false) then gameState.topOut = true gameState.antiControlRepeat["quit"] = true didSlowAction = true end end if checkControl("rotate_left", false) then mino.Rotate(-1, true) gameState.antiControlRepeat["rotate_left"] = true end if checkControl("rotate_right", false) then mino.Rotate(1, true) gameState.antiControlRepeat["rotate_right"] = true end end if checkControl("pause", false) then gameState.paused = not gameState.paused gameState.antiControlRepeat["pause"] = true end return didSlowAction end local tickTimer = os.startTimer(tickDelay) local evt local didControlTick = false while true do -- handle ghost piece ghostMino.color = "c" ghostMino.shape = mino.shape ghostMino.x = mino.x ghostMino.y = mino.y ghostMino.Move(0, board.height, true) -- render board render(true) evt = {os.pullEvent()} if evt[1] == "key" and not evt[3] then keysDown[evt[2]] = 1 didControlTick = controlTick(gameState, false) elseif evt[1] == "key_up" then keysDown[evt[2]] = nil end if evt[1] == "timer" then if evt[2] == tickTimer then tickTimer = os.startTimer(0.05) for k,v in pairs(keysDown) do keysDown[k] = 1 + v end controlTick(gameState, didControlTick) if not gameState.paused then tick(gameState) end didControlTick = false gameState.antiControlRepeat = {} end end if gameState.topOut then -- this will have a more elaborate game over sequence later return end end end term.clear() StartGame() term.setCursorPos(1, scr_y - 1) term.clearLine() print("Thank you for playing!") term.setCursorPos(1, scr_y - 0) term.clearLine() sleep(0.05)