ldd-CC/ldris2.lua

1486 lines
42 KiB
Lua
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[
,--,
,---.'|
| | : ,---, ,-.----. ,---, .--.--. ,----,
: : | .' .' `\ \ / \ ,`--.' | / / '. .' .' \
| ' : ,---.' \ ; : \ | : : | : /`. / ,----,' |
; ; ' | | .`\ | | | .\ : : | ' ; | |--` | : . ;
' | |__ : : | ' | . : |: | | : | | : ;_ ; |.' /
| | :.'| | ' ' ; : | | \ : ' ' ; \ \ `. `----'/ ;
' : ; ' | ; . | | : . / | | | `----. \ / ; /
| | ./ | | : | ' ; | | \ ' : ; __ \ \ | ; / /-,
; : ; ' : | / ; | | ;\ \ | | ' / /`--' / / / /.`|
| ,/ | | '` ,/ : ' | \.' ' : | '--'. / ./__; :
'---' ; : .' : : :-' ; |.' `--'---' | : .'
| ,.' | |.' '---' ; | .'
'---' `---' `---'
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 = 11, -- 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
}
_WRITE_TO_DEBUG_MONITOR = true
local cospc_debuglog = function(header, text)
if _WRITE_TO_DEBUG_MONITOR then
if ccemux then
ccemux.attach("right", "monitor")
local t = term.redirect(peripheral.wrap("right"))
if text == 0 then
term.clear()
term.setCursorPos(1, 1)
else
term.setTextColor(colors.yellow)
term.write(header or "SYS")
term.setTextColor(colors.white)
print(": " .. text)
end
term.redirect(t)
end
end
end
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
local roundToPlaces = function(number, places)
return math.floor(number * 10^places) / (10^places)
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
-- image-related functions (from NFTE)
local loadImageDataNFT = function(image, background) -- string image
local output = {{},{},{}} -- char, text, back
local y = 1
background = (background or "f"):sub(1,1)
local text, back = "f", background
local doSkip, c1, c2 = false
local tchar = string.char(31) -- for text colors
local bchar = string.char(30) -- for background colors
local maxX = 0
local bx
for i = 1, #image do
if doSkip then
doSkip = false
else
output[1][y] = output[1][y] or ""
output[2][y] = output[2][y] or ""
output[3][y] = output[3][y] or ""
c1, c2 = image:sub(i,i), image:sub(i+1,i+1)
if c1 == tchar then
text = c2
doSkip = true
elseif c1 == bchar then
back = c2
doSkip = true
elseif c1 == "\n" then
maxX = math.max(maxX, #output[1][y])
y = y + 1
text, back = " ", background
else
output[1][y] = output[1][y]..c1
output[2][y] = output[2][y]..text
output[3][y] = output[3][y]..back
end
end
end
for y = 1, #output[1] do
output[1][y] = output[1][y] .. (" "):rep(maxX - #output[1][y])
output[2][y] = output[2][y] .. (" "):rep(maxX - #output[2][y])
output[3][y] = output[3][y] .. (background):rep(maxX - #output[3][y])
end
return output
end
-- draws an image with the topleft corner at (x, y), with transparency
local drawImageTransparent = function(image, x, y, terminal)
terminal = terminal or term.current()
local cx, cy = terminal.getCursorPos()
local c, t, b
for iy = 1, #image[1] do
for ix = 1, #image[1][iy] do
c, t, b = image[1][iy]:sub(ix,ix), image[2][iy]:sub(ix,ix), image[3][iy]:sub(ix,ix)
if b ~= " " or c ~= " " then
terminal.setCursorPos(x + (ix - 1), y + (iy - 1))
terminal.blit(c, t, b)
end
end
end
terminal.setCursorPos(cx,cy)
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, blankColor)
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 = blankColor or "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
board.alignFromBottom = false
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, tY
if board.alignFromBottom then
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
else
tY = board.y
for y = 1 + (board.height - board.visibleHeight), board.height, 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 or (y + 0) <= (board.height - board.visibleHeight) then
colorLine1 = transparentLine
end
if (y + 1) > board.height or (y + 1) <= (board.height - board.visibleHeight) then
colorLine2 = transparentLine
end
if (y + 2) > board.height or (y + 2) <= (board.height - board.visibleHeight) then
colorLine3 = transparentLine
end
term.setCursorPos(board.x, board.y + tY)
term.blit(charLine2, colorLine1, colorLine2)
tY = tY + 1
term.setCursorPos(board.x, board.y + tY)
term.blit(charLine1, colorLine2, colorLine3)
tY = tY + 1
end
end
end
return board
end
local makeNewMino = function(minoTable, minoID, board, xPos, yPos, oldeMino)
local mino = oldeMino or {}
minoTable = minoTable or gameConfig.minos
if not minoTable[minoID] then
error("tried to spawn mino with invalid ID '" .. tostring(minoID) .. "'")
else
mino.shape = minoTable[minoID].shape
mino.spinID = minoTable[minoID].spinID
mino.kickID = minoTable[minoID].kickID
mino.color = minoTable[minoID].color
mino.name = minoTable[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 board and (
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.board and 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 mino, 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
if didMoveX or didMoveY 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
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
_G.makeNewMino = makeNewMino
local pseudoRandom = function(gameState)
return switch(gameConfig.randomBag) {
["random"] = function()
return math.random(1, #gameConfig.minos)
end,
["singlebag"] = function()
if #gameState.random_bag == 0 then
-- repopulate random bag
for i = 1, #gameConfig.minos do
if math.random(0, 1) == 0 then
gameState.random_bag[#gameState.random_bag + 1] = i
else
table.insert(gameState.random_bag, 1, i)
end
end
end
local pick = math.random(1, #gameState.random_bag)
local output = gameState.random_bag[pick]
table.remove(gameState.random_bag, pick)
return output
end,
["doublebag"] = function()
if #gameState.random_bag == 0 then
for r = 1, 2 do
-- repopulate random bag
for i = 1, #gameConfig.minos do
if math.random(0, 1) == 0 then
gameState.random_bag[#gameState.random_bag + 1] = i
else
table.insert(gameState.random_bag, 1, i)
end
end
end
end
local pick = math.random(1, #gameState.random_bag)
local output = gameState.random_bag[pick]
table.remove(gameState.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
gameState.linesCleared = gameState.linesCleared + #clearedLines
return clearedLines
end
local StartGame = function(player_number, native_control, board_xmod, board_ymod)
board_xmod = board_xmod or 0
board_ymod = board_ymod or 0
local gameState = {
gravity = gameConfig.startingGravity,
pNum = player_number,
targetPlayer = 0,
score = 0,
antiControlRepeat = {},
topOut = false,
canHold = true,
didHold = false,
heldPiece = false,
paused = false,
queue = {},
queueMinos = {},
linesCleared = 0,
random_bag = {},
gameTickCount = 0,
controlTickCount = 0,
controlsDown = {}, --
incomingGarbage = 0, -- amount of garbage that will be added to board after non-line-clearing mino placement
combo = 0, -- amount of successive line clears
backToBack = 0, -- amount of tetris/t-spins comboed
spinLevel = 0, -- 0 = no special spin
-- 1 = mini spin
-- 2 = Z/S/J/L spin
-- 3 = T spin
}
-- create boards
-- main gameplay board
gameState.board = makeNewBoard(
7 + board_xmod,
1 + board_ymod,
gameConfig.board_width, gameConfig.board_height
)
-- queue of upcoming minos
gameState.queueBoard = makeNewBoard(
gameState.board.x + gameState.board.width + 1,
gameState.board.y,
4,
28
--gameState.board.height - 12
)
-- display of currently held mino
gameState.holdBoard = makeNewBoard(
--gameState.board.x + gameState.board.width + 1,
2 + board_xmod,
--gameState.board.y + gameState.board.visibleHeight * (1/3),
1 + board_ymod,
gameState.queueBoard.width,
4
)
gameState.holdBoard.visibleHeight = 4
-- indicator of incoming garbage
gameState.garbageBoard = makeNewBoard(
gameState.board.x - 1,
gameState.board.y,
1,
gameState.board.visibleHeight,
"f"
)
gameState.garbageBoard.visibleHeight = gameState.garbageBoard.height
-- populate the queue
for i = 1, clientConfig.queue_length + 1 do
gameState.queue[i] = pseudoRandom(gameState)
end
for i = 1, clientConfig.queue_length do
gameState.queueMinos[i] = makeNewMino(nil,
gameState.queue[i + 1],
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(gameState)
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(nil,
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
local calculateGarbage = function(gameState, linesCleared)
local output = 0
local lncleartbl = {
[0] = 0,
[1] = 0,
[2] = 1,
[3] = 2,
[4] = 4,
[5] = 5,
[6] = 6,
[7] = 7,
[8] = 8
}
if (gameState.spinLevel == 3) or (gameState.spinLevel == 2 and gameConfig.spin_mode >= 2) then
output = output + linesCleared * 2
else
output = output + (lncleartbl[linesCleared] or 0)
end
-- add combo bonus
output = output + math.max(0, math.floor(-1 + gameState.combo / 2))
return output
end
local sendGameEvent = function(eventName, ...)
if native_control then
os.queueEvent(eventName, ...)
end
end
gameState.mino = makeDefaultMino(gameState)
local mino, board = gameState.mino, gameState.board
local holdBoard, queueBoard, garbageBoard = gameState.holdBoard, gameState.queueBoard, gameState.garbageBoard
local ghostMino = makeNewMino(nil, mino.minoID, gameState.board, mino.x, mino.y, {})
local garbageMinoShape = {}
for i = 1, garbageBoard.height do
garbageMinoShape[#garbageMinoShape + 1] = "@"
end
local garbageMino = makeNewMino({
[1] = {
shape = garbageMinoShape,
color = "e"
}
}, 1, garbageBoard, 1, garbageBoard.height + 1)
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))
garbageBoard.Render(garbageMino)
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(nil,
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(nil, 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(nil,
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
local linesCleared = handleLineClears(gameState)
if #linesCleared == 0 then
gameState.combo = 0
gameState.backToBack = 0
else
gameState.combo = gameState.combo + 1
if #linesCleared == 4 or gameState.spinLevel >= 1 then
gameState.backToBack = gameState.backToBack + 1
else
gameState.backToBack = 0
end
end
-- calculate garbage to be sent
local garbage = calculateGarbage(gameState, #linesCleared)
if garbage > 0 then
cospc_debuglog(gameState.pNum, "Doled out " .. garbage .. " lines")
end
-- send garbage to enemy player
sendGameEvent("attack", gameState.targetPlayer)
if doMakeNewMino then
gameState.spinLevel = 0
end
end
end
-- debug info
if native_control then
term.setCursorPos(2, scr_y - 2)
term.write("Lines: " .. gameState.linesCleared .. " ")
term.setCursorPos(2, scr_y - 1)
term.write("M=" .. mino.movesLeft .. ", TTL=" .. tostring(mino.lockTimer):sub(1, 4) .. " ")
term.setCursorPos(2, scr_y - 0)
term.write("POS=(" .. mino.x .. ":" .. tostring(mino.xFloat):sub(1, 5) .. ", " .. mino.y .. ":" .. tostring(mino.yFloat):sub(1, 5) .. ") ")
end
end
local checkControl = function(controlName, repeatTime, repeatDelay)
repeatDelay = repeatDelay or 1
if native_control then
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
else
if gameState.controlsDown[controlName] then
if not gameState.antiControlRepeat[controlName] then
if repeatTime then
return gameState.controlsDown[controlName] == 1 or
(
gameState.controlsDown[controlName] >= (repeatTime * (1 / tickDelay)) and (
repeatDelay and ((gameState.controlsDown[controlName] * tickDelay) % repeatDelay == 0) or true
)
)
else
return gameState.controlsDown[controlName] == 1
end
end
else
return false
end
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
if not mino.finished then
mino.Move(-1, 0, true, true)
didSlowAction = true
gameState.antiControlRepeat["move_left"] = true
end
end
if checkControl("move_right", clientConfig.move_repeat_delay, clientConfig.move_repeat_interval) then
if not mino.finished then
mino.Move(1, 0, true, true)
didSlowAction = true
gameState.antiControlRepeat["move_right"] = true
end
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
if not mino.finished then
mino.finished = 2
gameState.antiControlRepeat["hold"] = true
didSlowAction = true
end
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)
if mino.spinID <= gameConfig.spin_mode then
if (
mino.CheckCollision(1, 0) and
mino.CheckCollision(-1, 0) and
mino.CheckCollision(0, -1)
) then
gameState.spinLevel = 3
else
gameState.spinLevel = 0
end
end
gameState.antiControlRepeat["rotate_left"] = true
end
if checkControl("rotate_right", false) then
mino.Rotate(1, true)
if mino.spinID <= gameConfig.spin_mode then
if (
mino.CheckCollision(1, 0) and
mino.CheckCollision(-1, 0) and
mino.CheckCollision(0, -1)
) then
gameState.spinLevel = 3
else
gameState.spinLevel = 0
end
end
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)
garbageMino.y = 1 + garbageBoard.height - gameState.incomingGarbage
-- render board
render(true)
evt = {os.pullEvent()}
if evt[1] == "key" and not evt[3] then
keysDown[evt[2]] = 1
didControlTick = controlTick(gameState, false)
gameState.controlTickCount = gameState.controlTickCount + 1
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)
gameState.controlTickCount = gameState.controlTickCount + 1
if not gameState.paused then
tick(gameState)
gameState.gameTickCount = gameState.gameTickCount + 1
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
local TitleScreen = function()
local animation = function()
local tsx = 8
local tsy = 10
--[[
local title = {
[1] = "ee€\nee€\nee€fƒfe”",
[2] = "dd€fdf‚fd\ndd€  df•fd•\ndd€fƒfdŸ",
[3] = "11€f1ff1”\n11€f“‰f1\n11€  11€f•",
[4] = "affaŸ\naf•fa•\naf‚",
[5] = "3f—3€f3f\nf€3‹3f‚f3\n3f•ƒf3Ÿ",
[6] = "4f—f4Ÿ4f‚\n  4fŸf4‡\n4f—4€f‚ƒ"
}
--]]
--[[
1 = " ",
"@@@@",
" ",
" ",
2 = " @ ",
"@@@",
" ",
3 = " @",
"@@@",
" ",
4 = "@ ",
"@@@",
" ",
5 = "@@",
"@@",
6 = " @@",
"@@ ",
" ",
7 = "@@ ",
" @@",
" ",
]]
local animBoard = makeNewBoard(1, 1, scr_x, scr_y * 10/3, "f")
animBoard.visibleHeight = animBoard.height / 2
local animMinos = {}
local iterate = 0
local mTimer = 100000
local titleMinos = {
-- L
makeNewMino(nil, 4, animBoard, tsx + 1, tsy).Rotate(0),
makeNewMino(nil, 1, animBoard, tsx + 0, tsy).Rotate(3),
-- D
makeNewMino(nil, 7, animBoard, tsx + 6, tsy).Rotate(3),
makeNewMino(nil, 3, animBoard, tsx + 4, tsy).Rotate(1),
nil
}
for i = 1, #titleMinos do
if titleMinos[i] then
table.insert(animMinos, titleMinos[i])
end
end
while true do
iterate = (iterate + 10) % 360
if mTimer <= 0 then
table.insert(animMinos, makeNewMino(nil,
math.random(1, 7),
animBoard,
math.random(1, animBoard.width - 4),
animBoard.visibleHeight - 4
))
mTimer = 4
else
mTimer = mTimer - 1
end
for i = 1, #animMinos do
animMinos[i].Move(0, 0.75, false)
if animMinos[i].y > animBoard.height then
table.remove(animMinos, i)
end
end
animBoard.Render(table.unpack(animMinos))
sleep(0.05)
end
end
local menu = function()
local options = {"Singleplayer", "How to play", "Quit"}
end
--animation()
--StartGame(true, 0, 0)
parallel.waitForAny(function()
cospc_debuglog(1, "Starting game.")
StartGame(1, true, 0, 0)
cospc_debuglog(1, "Game concluded.")
end, function()
while true do
cospc_debuglog(2, "Starting game.")
StartGame(2, false, 24, 0)
cospc_debuglog(2, "Game concluded.")
end
end)
end
term.clear()
cospc_debuglog(nil, 0)
cospc_debuglog(nil, "Opened LDRIS2.")
TitleScreen()
cospc_debuglog(nil, "Closed LDRIS2.")
term.setCursorPos(1, scr_y - 1)
term.clearLine()
print("Thank you for playing!")
term.setCursorPos(1, scr_y - 0)
term.clearLine()
sleep(0.05)