ComputerCraft Archive

ldris2

computer utility LDDestroier github

Description

A collection of all my ComputerCraft programs and the APIs they use. This is mostly just to get them the fuck off of pastebin, and also to ensure that API owners don't change things to break my precious programs...!

Installation

Copy one of these commands into your ComputerCraft terminal:

wget:wget https://raw.githubusercontent.com/LDDestroier/CC/master/ldris2.lua ldris2
Archive:wget https://cc.shobie.xyz/cc/get/gh-LDDestroier-CC-ldris2 ldris2
Quick Install: wget https://cc.shobie.xyz/cc/get/gh-LDDestroier-CC-ldris2 ldris2

Usage

Run: ldris2

Tags

none

Source

View Original Source

Code Preview

--[[

   ,--,
,---.'|
|   | :        ,---,     ,-.----.       ,---,   .--.--.          ,----,
:   : |      .'  .' `\   \    /  \   ,`--.' |  /  /    '.      .'   .' \
|   ' :    ,---.'     \  ;   :    \  |   :  : |  :  /`. /    ,----,'    |
;   ; '    |   |  .`\  | |   | .\ :  :   |  ' ;  |  |--`     |    :  .  ;
'   | |__  :   : |  '  | .   : |: |  |   :  | |  :  ;_       ;    |.'  /
|   | :.'| |   ' '  ;  : |   |  \ :  '   '  ;  \  \    `.    `----'/  ;
'   :    ; '   | ;  .  | |   : .  /  |   |  |   `----.   \     /  ;  /
|   |  ./  |   | :  |  ' ;   | |  \  '   :  ;   __ \  \  |    ;  /  /-,
;   : ;    '   : | /  ;  |   | ;\  \ |   |  '  /  /`--'  /   /  /  /.`|
|   ,/     |   | '` ,/   :   ' | \.' '   :  | '--'.     /  ./__;      :
'---'      ;   :  .'     :   : :-'   ;   |.'    `--'---'   |   :    .'
           |   ,.'       |   |.'     '---'                 ;   | .'
           '---'         `---'                             `---'

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)
]]

_WRITE_TO_DEBUG_MONITOR = true

local scr_x, scr_y = term.getSize()

-- client config can be changed however you please
local clientConfig = {
	controls = {
		rotate_ccw = keys.z,		-- by left, I mean counter-clockwise
		rotate_cw = 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,	-- drop mino to bottom, but don't lock
		hold = keys.leftShift,
		pause = keys.p,
		restart = keys.r,
		open_chat = keys.t,
		quit = keys.q,
	},
	-- (SDF) the factor in which soft dropping effects the gravity
	soft_drop_multiplier = 4.0,
	
	-- (DAS) amount of time you must be holding the movement keys for it to start repeatedly moving (seconds)
	move_repeat_delay = 0.25,
	
	-- (ARR) speed at which the pieces move when holding the movement keys (seconds per tick)
	move_repeat_interval = 0.05,
	
	-- (ARE) amount of seconds it will take for the next piece to arrive after the current one locks into place
	appearance_delay = 0,
	
	-- (Lock Delay) amount of seconds it will take for a resting mino to lock into placed
	lock_delay = 0.5,
	
	-- amount of pieces visible in the queue (limited by size of UI)
	queue_length = 5,
}

-- 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 cospc_debuglog = function(header, text)
	if _WRITE_TO_DEBUG_MONITOR then
		if ccemux then
			if not peripheral.find("monitor") then
				ccemux.attach("right", "monitor")
			end
			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,
		animFrame = 0,
		state = "halt",
		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_ccw", 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_ccw"] = true
			end
			if checkControl("rotate_cw", 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_cw"] = 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)