ComputerCraft Archive

ldris

computer networking LDDestroier github

Description

## ##### ###### ###### ######

Installation

Copy one of these commands into your ComputerCraft terminal:

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

Usage

Run: ldris

Tags

networking

Source

View Original Source

Code Preview

--
-- ##     #####   ######  ######  ######
-- ##     ##  ##  ##   ##   ##   ##   ###
-- ##     ##   ## ##   ##   ##   ###
-- ##     ##   ## ######    ##    ######
-- ##     ##   ## ##   ##   ##        ###
-- ##     ##  ##  ##   ##   ##   ###   ##
-- #####  #####   ##   ## ######  ######
--
-- ComputerCraft port of Tetris
-- by LDDestroier
--
--  Supports wall kicking, holding, fast-dropping,
-- and ghost pieces.
--
-- TO-DO:
--  + Add random color pulsation (for effect!)
--  + Add a proper title screen. The current one is pathetic.

local sentInfos = 0

local scr_x, scr_y = term.getSize()
local keysDown = {}
local game = {
	p = {},					-- stores player information
	pp = {},				-- stores other player information that doesn't need to be sent during netplay
	you = 1,				-- current player slot
	instanceID = math.random(1, 2^31-1),	-- random per-instance value to ensure skynet doesn't poopie
	amountOfPlayers = 2,	-- amount of players for the current game
	running = true,			-- if set to false, will quit the game
	moveHoldDelay = 0.1,	-- amount of time to hold left or right for it to keep moving that way
	boardOverflow = 20,		-- amount of space above the board that it can overflow
	paused = false,			-- whether or not game is paused
	canPause = true,		-- if false, cannot pause game (such as in online multiplayer)
	inputDelay = 0.05,		-- amount of time between each input
	gameDelay = 0.05,		-- amount of time between game ticks
	minimumLockTimer = 0.4,	-- shortest amount of time before a piece locks upon touching the ground
	maximumLockTimer = 0.6,	-- longest amount of time before a piece locks upon touching the ground
	appearanceDelay = 0.05,	-- amount of time to wait after placing a piece
	config = {
		TGMlock = false,		-- replicate the piece locking from Tetris: The Grand Master
		scrubMode = false,	-- gives you nothing but I-pieces
	},
	control = {					-- client's control scheme
		moveLeft = keys.left,	-- shift left
		moveRight = keys.right,	-- shift right
		softDrop = keys.down,	-- shift downwards
		rotateLeft = keys.z,	-- rotate counter-clockwise
		rotateRight = keys.x,	-- rotate clockwise
		hardDrop = keys.up,		-- instantly drop and place piece
		sonicDrop = keys.space,	-- instantly drop piece, but not place
		hold = keys.leftShift,	-- slot piece into hold buffer
		quit = keys.q,			-- fuck off
		pause = keys.p
	},
	softDropSpeed = 2,			-- amount of spaces a soft drop will move a mino downwards
	revControl = {},			-- a mirror of "control", but with the keys and values inverted
	net = {									-- all network-related values
		isHost = true,						-- the host holds all the cards
		gameID = math.random(1, 2^31-1),	-- per-game ID to prevent interference from other games
		channel = 1294,						-- modem or skynet channel
		active = true,						-- whether or not you're using modems at all
		waitingForGame = true,				-- if you're waiting for another player to start the game
		modem = peripheral.find("modem"),	-- modem transmit object
		useSkynet = false,					-- if true, uses Skynet instead of modems
		skynet = nil,						-- skynet transmit object
		skynetURL = "https://raw.githubusercontent.com/osmarks/skynet/master/client.lua",	-- exactly what it looks like
		skynetPath = "/skynet.lua"			-- location for Skynet API
	},
	timers = {},
	timerNo = 1,
}

for k,v in pairs(game.control) do
	game.revControl[v] = k
end

game.setPaletteColor = function(...)
	if game.amountOfPlayers <= 1 then
		term.setPaletteColor(...)
	end
end

local getTime = function()
	if os.epoch then
		return os.epoch("utc")
	else
		return 24 * os.day() + os.time()
	end
end

game.startTimer = function(duration)
	game.timers[game.timerNo] = duration
	game.timerNo = game.timerNo + 1
	return game.timerNo - 1
end

game.cancelTimer = function(tID)
	game.timers[tID or 0] = nil
end

game.alterTimer = function(tID, mod)
	if game.timers[tID] then
		game.timers[tID] = game.timers[tID] + mod
	end
end

local tableCopy
tableCopy = function(tbl)
	local output = {}
	for k,v in pairs(tbl) do
		if type(v) == "table" then
			output[k] = tableCopy(v)
		else
			output[k] = v
		end
	end
	return output
end

-- sets up brown as colors.special, for palette swapping magic(k)
local tColors = tableCopy(colors)

tColors.white = 1
tColors.brown = nil	-- brown is now white
tColors.special = 4096
term.setPaletteColor(tColors.special, 0x353535)
term.setPaletteColor(tColors.white, 0xf0f0f0)

-- initializes and fixes up a board
-- boards are 2D objects that can display perfectly square graphics
local clearBoard = function(board, xpos, ypos, newXsize, newYsize, newBGcolor, topCull, clearContent)
	board = board or {}
	board.x = board.x or xpos or 1
	board.y = board.y or ypos or 1
	board.xSize = board.xSize or newXsize or 10
	board.ySize = board.ySize or newYsize or 24 + game.boardOverflow
	board.topCull = board.topCull or topCull or game.boardOverflow
	board.BGcolor = board.BGcolor or newBGcolor or "f"
	board.minoAmount = board.minoAmount or 0
	for y = 1, board.ySize do
		board[y] = board[y] or {}
		for x = 1, board.xSize do
			-- explanation on each space:
			-- {
			--   boolean; if true, the space is solid
			--   string; the hex color of the space
			--   number; the countdown until the space is made non-solid (inactive if 0)
			--   number; the countdown until the space is colored to board.BGcolor (inactive if 0)
			-- }
			if clearContent then
				board[y][x] = {false, board.BGcolor, 0, 0}
			else
				board[y][x] = board[y][x] or {false, board.BGcolor, 0, 0}
			end
		end
	end
	return board
end

-- tetramino information
-- don't tamper with this or I'll beat your ass so hard that war veterans would blush
local minos = {
	[1] = {	-- I-piece
		canRotate = true,
		canTspin = false,
		mainColor = "3",
		shape = {
			"    ",
			"3333",
			"    ",
			"    ",
		}
	},
	[2] = {	-- L-piece
		canRotate = true,
		canTspin = false,
		mainColor = "1",
		shape = {
			"  1",
			"111",
			"   ",
		}
	},
	[3] = {	-- J-piece
		canRotate = true,
		canTspin = false,
		mainColor = "b",
		shape = {
			"b  ",
			"bbb",
			"   ",
		}
	},
	[4] = {	-- O-piece
		canRotate = true,
		canTspin = false,
		mainColor = "4",
		shape = {
			"44",
			"44",
		}
	},
	[5] = { -- T-piece
		canRotate = true,
		canTspin = true,
		mainColor = "a",
		shape = {
			" a ",
			"aaa",
			"   ",
		}
	},
	[6] = { -- Z-piece
		canRotate = true,
		canTspin = false,
		mainColor = "e",
		shape = {
			"ee ",
			" ee",
			"   ",
		}
	},
	[7] = {	-- S-piece
		canRotate = true,
		canTspin = false,
		mainColor = "5",
		shape = {
			" 55",
			"55 ",
			"   ",
		}
	},
	["gameover"] = {	-- special "mino" for game over
		canRotate = false,
		shape = {
			" ccc   ccc  c   c ccccc    ccc  c   c ccccc  cccc",
			"c     c   c cc cc c       c   c c   c c     c   c",
			"c     c   c cc cc c       c   c c   c c     c   c",
			"c  cc ccccc c c c cccc    c   c  c c  cccc   cccc",
			"c   c c   c c   c c       c   c  c c  c     c   c",
			"c   c c   c c   c c       c   c  c c  c     c   c",
			"c   c c   c c   c c       c   c  c c  c     c   c",
			" ccc  c   c c   c ccccc    ccc    c   ccccc c   c",
		}
	},
	["yousuck"] = {
		canRotate = false,
		shape = {
			"c   c  ccc  c   c    ccc  c   c  ccc  c   c",
			"c   c c   c c   c   c   c c   c c   c c  c ",
			"c   c c   c c   c   c     c   c c     c c  ",
			" c c  c   c c   c    ccc  c   c c     cc   ",
			"  c   c   c c   c       c c   c c     c c  ",
			"  c   c   c c   c       c c   c c     c  c ",
			"  c   c   c c   c   c   c c   c c   c c   c",
			"  c    ccc   ccc     ccc   ccc   ccc  c   c",
		}
	},
	["eatmyass"] = {
		canRotate = false,
		shape = {
			"ccccc  ccc  ccccc   c     c c     c    ccc   ccc   ccc ",
			"c     c   c   c     cc   cc  c   c    c   c c   c c   c",
			"c     c   c   c     c c c c   c c     c   c c     c    ",
			"cccc  ccccc   c     c  c  c    c      ccccc  ccc   ccc ",
			"c     c   c   c     c     c    c      c   c     c     c",
			"c     c   c   c     c     c    c      c   c     c     c",
			"c     c   c   c     c     c    c      c   c c   c c   c",
			"ccccc c   c   c     c     c    c      c   c  ccc   ccc ",
		}
	},
	["nice"] = {	-- nice
		canRotate = false,
		shape = {
			"        c                ",
			"                         ",
			"c  ccc  c  cccc   cccc   ",
			"c c   c c c    c c    c  ",
			"cc    c c c      c    c  ",
			"c     c c c      cccccc  ",
			"c     c c c      c       ",
			"c     c c c      c       ",
			"c     c c c    c c    c  ",
			"c     c c  cccc   cccc  c",
		}
	}
}

for k,mino in pairs(minos) do
	minos[k].size = 0
	for y = 1, #mino.shape do
		for x = 1, #mino.shape[y] do
			if mino.shape[y]:sub(x, x) ~= " " then
				minos[k].size = minos[k].size + 1
			end
		end
	end
end

local images = {
	-- to do...add images...
}

-- converts blit colors to colors api, and back
local to_colors, to_blit = {
	[' '] = 0,
	['0'] = 1,
	['1'] = 2,
	['2'] = 4,
	['3'] = 8,
	['4'] = 16,
	['5'] = 32,
	['6'] = 64,
	['7'] = 128,
	['8'] = 256,
	['9'] = 512,
	['a'] = 1024,
	['b'] = 2048,
	['c'] = 4096,
	['d'] = 8192,
	['e'] = 16384,
	['f'] = 32768,
}, {}
for k,v in pairs(to_colors) do
	to_blit[v] = k
end

-- checks if (x, y) is a valid space on the board
local doesSpaceExist = function(board, x, y)
	return (x >= 1 and x <= board.xSize) and (y >= 1 and y <= board.ySize)
end

-- checks if (x, y) is being occupied by a tetramino (or if it's off-board)
local isSpaceSolid = function(board, _x, _y)
	local x, y = math.floor(_x), math.floor(_y)
	if doesSpaceExist(board, x, y) then
		return board[y][x][1]
	else
		return true
	end
end

-- ticks down a space's timers, which can cause it to become non-solid or background-colored
local ageSpace = function(board, _x, _y)
	local x, y = math.floor(_x), math.floor(_y)
	if doesSpaceExist(board, x, y) then
		-- make space non-solid if timer elapses
		if board[y][x][3] ~= 0 then
			board[y][x][3] = board[y][x][3] - 1
			if board[y][x][3] == 0 then
				board[y][x][1] = false
			end
		end
		-- color space board.BGcolor if timer elapses
		if board[y][x][4] ~= 0 then
			board[y][x][4] = board[y][x][4] - 1
			if board[y][x][4] == 0 then
				board[y][x][2] = board.BGcolor
			end
		end
	end
end

local transmit = function(msg)
	if game.net.active then
		if game.net.useSkynet then
			game.net.skynet.send(game.net.channel, msg)
		else
			game.net.modem.transmit(game.net.channel, game.net.channel, msg)
		end
	end
end

local sendInfo = function(command, doSendTime, playerNumber)
	sentInfos = sentInfos + 1
	if game.net.isHost then
		transmit({
			command = command,
			gameID = game.net.gameID,
			time = doSendTime and getTime(),
			p = playerNumber and game.p[playerNumber] or game.p,
			pNum = playerNumber,
			you = game.you,
			specialColor = {term.getPaletteColor(tColors.special)}
		})
	else
		transmit({
			command = command,
			gameID = game.net.gameID,
			time = doSendTime and getTime(),
			pNum = playerNumber,
			you = game.you,
			control = game.p[game.you].control
		})
	end
end

-- generates a "mino" object, which can be drawn and manipulated on a board
local makeNewMino = function(minoType, board, x, y, replaceColor)
	local mino = tableCopy(minos[minoType])
	if replaceColor then
		for yy = 1, #mino.shape do
			mino.shape[yy] = mino.shape[yy]:gsub("[^ ]", replaceColor)
		end
	end
	-- what color the ghost mino will be
	mino.ghostColor = 0x353535

	mino.x = x
	mino.y = y
	mino.didWallKick = false
	mino.color = minos[minoType].mainColor or "c"
	mino.didTspin = false	-- if the player has done a T-spin with this piece
	mino.lockBreaks = 16	-- anti-infinite measure
	mino.waitingForLock = false
	mino.board = board
	mino.minoType = minoType
	-- checks to see if the mino is currently clipping with a solid board space (with the offset values)
	mino.checkCollision = function(xOffset, yOffset, doNotCountBorder)
		local cx, cy
		for y = 1, #mino.shape do
			for x = 1, #mino.shape[y] do
				cx = mino.x + x + (xOffset or 0)
				cy = mino.y + y + (yOffset or 0)
				if mino.shape[y]:sub(x,x) ~= " " then
					if isSpaceSolid(mino.board, cx, cy) and not (doNotCountBorder and not doesSpaceExist(board, cx, cy)) then
						return true
					end
				end
			end
		end
		return false
	end
	-- rotates a mino, and kicks it off a wall if need be
	mino.rotate = function(direction)
		local output = {}
		local oldShape = tableCopy(mino.shape)
		local origX, origY = mino.x, mino.y
		for y = 1, #mino.shape do
			output[y] = {}
			for x = 1, #mino.shape[y] do
				if direction == 1 then
					output[y][x] = mino.shape[#mino.shape - (x - 1)]:sub(y,y)
				elseif direction == -1 then
					output[y][x] = mino.shape[x]:sub(-y, -y)
				else
					error("invalid rotation direction (must be 1 or -1)")
				end
			end
			output[y] = table.concat(output[y])
		end
		mino.shape = output
		-- check T-spin
		local checkTspin = function(mino)
			if (mino.checkCollision(-1, 0, false) and mino.checkCollision(1, 0, false) and mino.checkCollision(0, -1, false)) then
				if mino.didWallKick then
					mino.didTspin = 1
				else
					mino.didTspin = 2
				end
			else
				mino.didTspin = false
			end
			return mino.didTspin
		end
		-- try to kick off wall/floor
		if mino.checkCollision(0, 0) then
			-- try T-spin triple rotation
			if not mino.checkCollision(-direction, 2) then
				mino.y = mino.y + 2
				mino.x = mino.x - direction
				mino.didWallKick = true
				checkTspin(mino)
				return true
			end
			-- kick off ceiling
			for y = 1, #mino.shape do
				for x = -1, 1 do
					if not mino.checkCollision(x, y) then
						mino.x = mino.x + x
						mino.y = mino.y + y
						mino.didWallKick = true

						--checkTspin(mino)
						--return true
					end
				end
			end
			-- kick off floor
			for y = 1, math.floor(#mino.shape / 2) do
				if not mino.checkCollision(0, -y) then
					mino.y = mino.y - y
					mino.didWallKick = true
					checkTspin(mino)
					return true
				elseif not mino.checkCollision(-1, -y) then
					mino.y = mino.y - y
					mino.x = mino.x - 1
					mino.didWallKick = true
					checkTspin(mino)
					return true
				elseif not mino.checkCollision(1, -y) then
					mino.y = mino.y - y
					mino.x = mino.x + 1
					mino.didWallKick = true
					checkTspin(mino)
					return true
				end
			end
			-- kick off right wall
			for x = 0, -math.floor(#mino.shape[1] / 2), -1 do
				if not mino.checkCollision(x, 0) then
					mino.x = mino.x + x
					checkTspin(mino)
					return true
				end
				-- try diagonal-down
				if not mino.checkCollision(x, 1) then
					mino.x = mino.x + x
					mino.y = mino.y + 1
					sendInfo("send_info", false)
					checkTspin(mino)
					return true
				end
			end
			-- kick off left wall
			for x = 0, math.floor(#mino.shape[1] / 2) do
				if not mino.checkCollision(x, 0) then
					mino.x = mino.x + x
					mino.didWallKick = true
					checkTspin(mino)
					return true
				end
				-- try diagonal-down
				if not mino.checkCollision(x, 1) then
					mino.x = mino.x + x
					mino.y = mino.y + 1
					sendInfo("send_info", false)
					mino.didWallKick = true
					checkTspin(mino)
					return true
				end
			end
			mino.shape = oldShape
			return false
		else
			checkTspin(mino)
			return true
		end
	end
	-- draws a mino onto a board; you'll still need to render the board, though
	mino.draw = function(isSolid, colorReplace)
		for y = 1, #mino.shape do
			for x = 1, #mino.shape[y] do
				if mino.shape[y]:sub(x,x) ~= " " then
					if doesSpaceExist(mino.board, x + math.floor(mino.x), y + math.floor(mino.y)) then
						mino.board[y + math.floor(mino.y)][x + math.floor(mino.x)] = {
							isSolid or false,
							colorReplace or mino.shape[y]:sub(x,x),
							isSolid and 0 or 0,
							isSolid and 0 or 1
						}
					end
				end
			end
		end
	end
	-- moves a mino, making sure not to clip with solid board spaces
	mino.move = function(x, y, doSlam)
		if not mino.checkCollision(x, y) then
			if not (x == 0 and y == 0) then
				mino.x = mino.x + x
				mino.y = mino.y + y
				mino.didTspin = false
				mino.didWallKick = false
			end
			return true
		elseif doSlam then
			for sx = 0, x, math.abs(x) / x do
				if mino.checkCollision(sx, 0) then
					mino.x = mino.x + sx - math.abs(x) / x
					break
				end
				if sx ~= 0 then
					mino.didWallKick = false
					mino.didTspin = false
				end
			end
			for sy = 0, math.ceil(y), math.abs(y) / y do
				if mino.checkCollision(0, sy) then
					mino.y = mino.y + sy - math.abs(y) / y
					break
				end
				if sy ~= 0 then
					mino.didWallKick = false
				end
			end
		else
			return false
		end
	end

	return mino
end

-- generates a random number, excluding those listed in the _psExclude table
local pseudoRandom = function(randomPieces)
	if game.config.scrubMode then
		return 1
	else
		if #randomPieces == 0 then
			for i = 1, #minos do
				randomPieces[i] = i
			end
		end
		local rand = math.random(1, #randomPieces)
		local num = randomPieces[rand]
		table.remove(randomPieces, rand)
		return num
	end
end

-- initialize players
local initializePlayers = function(amountOfPlayers)
	local newPlayer = function(xmod, ymod)
		return {
			xmod = xmod,
			ymod = ymod,
			control = {},
			board = clearBoard({}, 2 + xmod, 2 + ymod, 10, nil, "f"),
			holdBoard = clearBoard({}, 13 + xmod, 14 + ymod, 4, 3, "f", 0),
			queueBoard = clearBoard({}, 13 + xmod, 2 + ymod, 4, 14, "f", 0),
			randomPieces = {},			-- list of all minos for pseudo-random selection
			flashingSpecial = false,	-- if true, then this player is flashing the special color
		}, {
			frozen = false,		-- if true, literally cannot move or act
			hold = 0,			-- current piece being held
			canHold = true,		-- whether or not player can hold (can't hold twice in a row)
			queue = {},			-- current queue of minos to use
			garbage = 0,		-- amount of garbage you'll get after the next drop
			lines = 0,			-- amount of lines cleared, "points"
			combo = 0,			-- amount of consequative line clears
			drawCombo = false,	-- draw the combo message
			lastLinesClear = 0,	-- previous amount of simultaneous line clears (does not reset if miss)
			level = 1,			-- level determines speed of mino drop
			fallSteps = 0.1,	-- amount of spaces the mino will draw each drop
		}
	end

	game.p = {}
	game.pp = {}

	for i = 1, (amountOfPlayers or 1) do
		game.p[i], game.pp[i] = newPlayer((scr_x/2 - 8 * amountOfPlayers) + (i - 1) * 18, 0)
	end

	-- generates the initial queue of minos per player
	for p = 1, #game.pp do
		for i = 1, #minos do
			game.pp[p].queue[i] = pseudoRandom(game.p[p].randomPieces)
		end
	end
end

-- actually renders a board to the screen
local renderBoard = function(board, bx, by, doAgeSpaces, blankColor)
	local char, line
	local tY = board.y + (by or 0)
	for y = (board.topCull or 0) + 1, board.ySize, 3 do
		line = {("\143"):rep(board.xSize),"",""}
		term.setCursorPos(board.x + (bx or 0), tY)
		for x = 1, board.xSize do
			line[2] = line[2] .. (blankColor or board[y][x][2])
			if board[y + 1] then
				line[3] = line[3] .. (blankColor or board[y + 1][x][2])
			else
				line[3] = line[3] .. board.BGcolor
			end
		end
		term.blit(line[1], line[2], line[3])
		line = {("\131"):rep(board.xSize),"",""}
		term.setCursorPos(board.x + (bx or 0), tY + 1)
		for x = 1, board.xSize do
			if board[y + 2] then
				line[2] = line[2] .. (blankColor or board[y + 1][x][2])
				line[3] = line[3] .. (blankColor or board[y + 2][x][2])
			elseif board[y + 1] then
				line[2] = line[2] .. (blankColor or board[y + 1][x][2])
				line[3] = line[3] .. board.BGcolor
			else
				line[2] = line[2] .. board.BGcolor
				line[3] = line[3] .. board.BGcolor
			end
		end
		term.blit(line[1], line[2], line[3])
		tY = tY + 2
	end
	if doAgeSpaces then
		for y = 1, board.ySize do
			for x = 1, board.xSize do
				ageSpace(board, x, y)
			end
		end
	end
end

-- checks if you've done the one thing in tetris that you need to be doing
local checkIfLineCleared = function(board, y)
	for x = 1, board.xSize do
		if not board[y][x][1] then
			return false
		end
	end
	return true
end

-- draws the score of a player, and clears the space where the combo text is drawn
local drawScore = function(player, cPlayer)
	if not cPlayer.drawCombo then
		term.setCursorPos(2 + player.xmod, 18 + player.ymod)
		term.setTextColor(tColors.white)
		term.write((" "):rep(14))
		term.setCursorPos(2 + player.xmod, 18 + player.ymod)
		term.write("Lines: " .. cPlayer.lines)
		term.setCursorPos(2 + player.xmod, 19 + player.ymod)
		term.write((" "):rep(14))
	end
end

local drawLevel = function(player, cPlayer)
	term.setCursorPos(13 + player.xmod, 17 + player.ymod)
	term.write("Lv" .. cPlayer.level .. "  ")
end

-- draws the player's simultaneous line clear after clearing one or more lines
-- also tells the player's combo, which is nice
local drawComboMessage = function(player, cPlayer, lines, didTspin, didDoAllClear)
	local msgs = {
		[0] = "",
		"SINGLE",
		"DOUBLE",
		"TRIPLE",
		"TETRIS"
	}
	if not msgs[lines] then
		return
	end
	term.setCursorPos(player.xmod + 2, player.ymod + 18)
	term.setTextColor(tColors.white)
	term.write((" "):rep(16))
	term.setCursorPos(player.xmod + 2, player.ymod + 18)
	if didDoAllClear then
		term.write("ALL CLEAR!! ")
	else
		if didTspin then
			if didTspin == 1 then
				term.write("T-SPIN MINI ")
			elseif didTspin == 2 then
				term.write("T-SPIN ")
			end
		else
			if lines == cPlayer.lastLinesCleared then
				if lines == 3 then
					term.write("OH BABY A ")
				else
					term.write("ANOTHER ")
				end
			end
		end
		term.write(msgs[lines])
	end
	if cPlayer.combo >= 2 and lines > 0 then
		term.setCursorPos(player.xmod + 2, player.ymod + 19)
		term.setTextColor(tColors.white)
		term.write((" "):rep(16))
		term.setCursorPos(player.xmod + 2, player.ymod + 19)
		if lines == 4 and cPlayer.combo == 3 then
			term.write("HOLY SHIT!")
		elseif lines == 4 and cPlayer.combo > 3 then
			term.write("ALRIGHT JACKASS")
		else
			term.write(cPlayer.combo .. "x COMBO")
		end
	end

end

-- god damn it you've fucked up
local gameOver = function(player, cPlayer)
	local mino
	local waitTime
	if cPlayer.lines == 0 then
		mino = makeNewMino("eatmyass", player.board, 12, 3 + game.boardOverflow)
		waitTime = 70
	elseif cPlayer.lines <= 5 then
		mino = makeNewMino("yousuck", player.board, 12, 3 + game.boardOverflow)
		waitTime = 60
	elseif cPlayer.lines == 69 or cPlayer.lines == 690 then
		mino = makeNewMino("nice", player.board, 12, 3 + game.boardOverflow)
		waitTime = 50
	else
		mino = makeNewMino("gameover", player.board, 12, 3 + game.boardOverflow)
		waitTime = 70
	end
	local color = 0
	local evt
	local sTimer
	local cTimer = os.startTimer(1)
	local canSkip = false
	for i = 1, waitTime do
		mino.x = mino.x - 1
		mino.draw()
		sendInfo("send_info", false)
		renderBoard(player.board, 0, 0, true)
		for i = 1, 20 do
			color = color + 0.01
			game.setPaletteColor(4096, math.sin(color) / 2 + 0.5, math.sin(color) / 2, math.sin(color) / 2)
		end

		sTimer = os.startTimer(0.05)
		while true do
			evt = {os.pullEvent()}
			if evt[1] == "timer" then
				if evt[2] == sTimer then
					break
				elseif evt[2] == cTimer then
					canSkip = true
				end
			elseif evt[1] == "key" then
				if canSkip then
					return
				end
			end
		end
	end
	return
end

-- used when darkening just-placed pieces
local normalPalettes = {}
local darkerPalettes = {}
for i = 1, 16 do
	normalPalettes[("0123456789abcdef"):sub(i,i)] = {term.getPaletteColor(2 ^ (i - 1))}
end
local genDarkerPalettes = function(mult)
	for i = 1, 16 do
		darkerPalettes[("0123456789abcdef"):sub(i,i)] = {
			normalPalettes[("0123456789abcdef"):sub(i,i)][1] * (mult or 0.6),
			normalPalettes[("0123456789abcdef"):sub(i,i)][2] * (mult or 0.6),
			normalPalettes[("0123456789abcdef"):sub(i,i)][3] * (mult or 0.6),
		}
	end
end
genDarkerPalettes()

-- calculates the amount of garbage to send
local calculateGarbage = function(lines, combo, backToBack, didTspin)
	local output = 0
	local clearTbl = {}
	if didTspin then
		clearTbl = {
			2,
			4,
			6,
			8,
			10,
			12,
			14,
		}
	else
		clearTbl = {
			0,
			1,
			2,
			4,
			6,
			8,
			10
		}
	end
	return (clearTbl[lines] or 0) + backToBack + math.max(0, combo - 2)
end

-- actually give a player some garbage
local doleOutGarbage = function(player, cPlayer, amount)
	local board = player.board
	local gx = math.random(1, board.xSize)
	local repeatProbability = 75	-- percent probability that garbage will leave the same hole open
	for i = 1, amount do
		table.remove(player.board, 1)
		player.board[board.ySize] = {}
		for x = 1, board.xSize do
			if x ~= gx then
				player.board[board.ySize][x] = {true, "8", 0, 0}
			else
				player.board[board.ySize][x] = {false, board.BGcolor, 0, 0}
			end
		end
		if math.random(0, 100) > repeatProbability then
			gx = math.random(1, board.xSize)
		end
		board.minoAmount = board.minoAmount + board.xSize - 1
	end
	cPlayer.garbage = 0
end

local ldPalettes = function(light)
	if light then
		for k,v in pairs(normalPalettes) do
			if k == "c" then
				--term.setPaletteColor(to_colors[k], 0xf0f0f0)
			else
				game.setPaletteColor(to_colors[k], table.unpack(v))
			end
		end
	else
		for i = 1, 0.3, -0.1 do
			genDarkerPalettes(i)
			for k,v in pairs(darkerPalettes) do
				if k == "c" then
					--game.setPaletteColor(to_colors[k], 0xf0f0f0)
				else
					game.setPaletteColor(to_colors[k], table.unpack(v))
				end
			end
			sleep(0.05)
		end
	end
	genDarkerPalettes()
end

-- initiates a game as a specific player (takes a number)
local startGame = function(playerNumber)

	local mino, ghostMino
	local dropTimer, inputTimer, lockTimer, tickTimer, comboTimer
	local evt, board, player, cPlayer, control
	local finished			-- whether or not a mino is done being placed
	local clearedLines = {}	-- used when calculating cleared lines

	if game.net.isHost then

		player = game.p[playerNumber]
		cPlayer = game.pp[playerNumber]
		board = player.board
		control = player.control

		local draw = function(isSolid, doSendInfo)
			local canChangeSpecial = true
			for k,v in pairs(game.p) do
				if v.flashingSpecial then
					canChangeSpecial = false
					break
				end
			end
			if canChangeSpecial then
				game.setPaletteColor(4096, mino.ghostColor)
			end
			ghostMino.x = mino.x
			ghostMino.y = mino.y
			ghostMino.move(0, board.ySize, true)
			ghostMino.draw(false)
			mino.draw(isSolid, ageSpaces)
			if doSendInfo then
				sendInfo("send_info", false, playerNumber)
			end
			renderBoard(board, 0, 0, true)
			renderBoard(player.queueBoard, 0, 0, false)
		end

		local currentMinoType
		local takeFromQueue = true

		local interpretInput = function()
			finished = false
			game.cancelTimer(inputTimer)
			inputTimer = game.startTimer(game.inputDelay)

			if control.quit == 1 then
				finished = true
				game.running = false
				game.paused = false
				ldPalettes(true)
				sendInfo("quit_game", false)
				return
			end

			if game.paused then
				if control.pause == 1 then
					ldPalettes(true)
					game.paused = false
				end
			else
				if control.pause == 1 then
					ldPalettes(false)
					game.paused = true
				end
				if not cPlayer.frozen then
					if control.moveLeft == 1 or (control.moveLeft or 0) > 1 + game.moveHoldDelay then
						if mino.move(-1, 0) then
							game.cancelTimer(lockTimer or 0)
							mino.waitingForLock = false
							draw(nil, true)
						end
					end
					if control.moveRight == 1 or (control.moveRight or 0) >= 1 + game.moveHoldDelay then
						if mino.move(1, 0) then
							game.cancelTimer(lockTimer or 0)
							mino.waitingForLock = false
							draw(nil, true)
						end
					end
					if control.softDrop then
						game.cancelTimer(lockTimer or 0)
						mino.waitingForLock = false
						local oldY = mino.y
						mino.move(0, game.softDropSpeed, true)
						if mino.y ~= oldY then
							draw(nil, true)
						else
							if game.config.TGMlock then
								draw(true, true)
								cPlayer.canHold = true
								finished = true
								board.minoAmount = board.minoAmount + mino.size
							else
								if mino.waitingForLock then
									game.alterTimer(lockTimer, -0.1)
								else
									mino.lockBreaks = mino.lockBreaks - 1
									lockTimer = game.startTimer(math.min(math.max(0.2 / cPlayer.fallSteps, game.minimumLockTimer), game.maximumLockTimer))
									mino.waitingForLock = true
								end
							end
						end
					end
					if control.rotateLeft == 1 then
						if mino.rotate(-1) then
							ghostMino.y = mino.y
							ghostMino.rotate(-1)
							game.cancelTimer(lockTimer or 0)
							mino.waitingForLock = false
							draw(nil, true)
						end
					end
					if control.rotateRight == 1 then
						if mino.rotate(1) then
							ghostMino.y = mino.y
							ghostMino.rotate(1)
							game.cancelTimer(lockTimer or 0)
							mino.waitingForLock = false
							draw(nil, true)
						end
					end
					if control.hold == 1 then
						if cPlayer.canHold then
							if cPlayer.hold == 0 then
								takeFromQueue = true
							else
								takeFromQueue = false
							end
							cPlayer.hold, currentMinoType = currentMinoType, cPlayer.hold
							cPlayer.canHold = false
							player.holdBoard = clearBoard(player.holdBoard, nil, nil, nil, nil, nil, nil, true)
							makeNewMino(
								cPlayer.hold,
								player.holdBoard,
								#minos[cPlayer.hold].shape[1] == 2 and 1 or 0,
								0
							).draw(false)
							sendInfo("send_info", false, playerNumber)
							renderBoard(player.holdBoard, 0, 0, false)
							finished = true
							cPlayer.didHold = true
						end
					end
					if control.hardDrop == 1 then
						mino.move(0, board.ySize, true)
						if game.config.TGMlock then
							draw(false)
						else
							draw(true, true)
							cPlayer.canHold = true
							finished = true
							board.minoAmount = board.minoAmount + mino.size
						end
					elseif control.sonicDrop == 1 then
						mino.move(0, board.ySize, true)
						draw(false)
					end
				end
			end
			for k,v in pairs(player.control) do
				player.control[k] = v + game.inputDelay
			end
		end

		renderBoard(player.holdBoard, 0, 0, true)

		while game.running do

			cPlayer.level = math.ceil((1 + cPlayer.lines) / 10)
			cPlayer.fallSteps = 0.075 * (1.33 ^ cPlayer.level)
			cPlayer.didHold = false

			if takeFromQueue then
				currentMinoType = cPlayer.queue[1]
			end

			mino = makeNewMino(
				currentMinoType,
				board,
				math.floor(board.xSize / 2) - 2,
				game.boardOverflow - 1
			)
			if mino.checkCollision() then
				mino.y = mino.y - 1
			end
			cPlayer.mino = mino

			ghostMino = makeNewMino(
				currentMinoType,
				board,
				math.floor(board.xSize / 2) - 2,
				game.boardOverflow - 1,
				"c"
			)
			cPlayer.ghostMino = ghostMino

			if takeFromQueue then
				table.remove(cPlayer.queue, 1)
				table.insert(cPlayer.queue, pseudoRandom(player.randomPieces))
			end

			-- draw queue
			player.queueBoard = clearBoard(player.queueBoard, nil, nil, nil, nil, nil, nil, true)
			for i = 1, math.min(#cPlayer.queue, 4) do
				local m = makeNewMino(
					cPlayer.queue[i],
					player.queueBoard,
					#minos[cPlayer.queue[i]].shape[1] == 2 and 1 or 0,
					1 + (3 * (i - 1)) + (i > 1 and 2 or 0)
				)
				m.draw()
			end

			-- draw held piece
			if cPlayer.hold ~= 0 then
				local m = makeNewMino(
					cPlayer.hold,
					player.holdBoard,
					#minos[cPlayer.hold].shape[1] == 2 and 1 or 0,
					0
				)
			end

			takeFromQueue = true

			drawScore(player, cPlayer)
			drawLevel(player, cPlayer)

			term.setCursorPos(13 + player.xmod, 13 + player.ymod)
			term.write("HOLD")

			-- check to see if you've topped out
			if mino.checkCollision() then
				for k,v in pairs(game.pp) do
					game.pp[k].frozen = true
				end
				sendInfo("game_over", false)
				gameOver(player, cPlayer)
				sendInfo("quit_game", false)
				return
			end

			mino.draw()
			ghostMino.move(0, board.ySize, true)
			ghostMino.draw()
			sendInfo("send_info", false, playerNumber)

			draw()


			game.cancelTimer(dropTimer)
			game.cancelTimer(lockTimer)

			dropTimer = game.startTimer(0)
			inputTimer = game.startTimer(game.inputDelay)
			game.cancelTimer(lockTimer or 0)

			tickTimer = os.startTimer(game.inputDelay)

			-- drop a piece
			while game.running do

				evt = {os.pullEvent()}

				control = game.p[playerNumber].control

				-- tick down internal game timer system
				if evt[1] == "timer" then
					if evt[2] == tickTimer then
						--local delKeys = {}
						for k,v in pairs(game.timers) do
							game.timers[k] = v - 0.05
							if v <= 0 then
								os.queueEvent("gameTimer", k)
								game.timers[k] = nil
							end
						end
						tickTimer = os.startTimer(game.inputDelay)
					elseif evt[2] == comboTimer then
						cPlayer.drawCombo = false
						drawScore(player, cPlayer)
					end
				end

				if player.paused then
					if evt[1] == "gameTimer" then
						if control.pause == 1 then
							game.paused = false
						end
					end
				else
					if evt[1] == "key" and evt[3] == false then

						interpretInput()
						if finished then
							break
						end

					elseif evt[1] == "gameTimer" then

						if evt[2] == inputTimer then

							interpretInput()
							if finished then
								break
							end

						elseif evt[2] == dropTimer then
							dropTimer = game.startTimer(0)
							if not game.paused then
								if not cPlayer.frozen then
									mino.oldY = mino.y
									if mino.checkCollision(0, 1) then
										if mino.lockBreaks == 0 then
											draw(true)
											cPlayer.canHold = true
											break
										elseif not mino.waitingForLock then
											mino.lockBreaks = mino.lockBreaks - 1
											lockTimer = game.startTimer(math.min(math.max(0.2 / cPlayer.fallSteps, game.minimumLockTimer), game.maximumLockTimer))
											mino.waitingForLock = true
										end
									else
										mino.move(0, cPlayer.fallSteps, true)
										if math.floor(mino.oldY) ~= math.floor(mino.y) then
											draw(false, true)
											--sendInfo("send_info", false)
										end
										draw()
									end
								end
							end
						elseif evt[2] == lockTimer then
							if not game.paused then
								cPlayer.canHold = true
								draw(true)
								break
							end
						end
					end
				end
			end

			clearedLines = {}
			for y = 1, board.ySize do
				if checkIfLineCleared(board, y) then
					table.insert(clearedLines, y)
					board.minoAmount = board.minoAmount - board.xSize
				end
			end
			if #clearedLines == 0 then
				if cPlayer.canHold then
					cPlayer.combo = 0
				end
				if mino.didTspin then
					cPlayer.drawCombo = true
					os.cancelTimer(comboTimer or 0)
					comboTimer = os.startTimer(2)
					drawComboMessage(player, cPlayer, #clearedLines, mino.didTspin)
				end
			else
				cPlayer.combo = cPlayer.combo + 1
				cPlayer.lines = cPlayer.lines + #clearedLines
				cPlayer.drawCombo = true
				os.cancelTimer(comboTimer or 0)
				comboTimer = os.startTimer(2)
				if cPlayer.lastLinesCleared == #clearedLines and #clearedLines >= 3 then
					player.backToBack = player.backToBack + 1
				else
					player.backToBack = 0
				end

				-- check for all clear
				local didDoAllClear = board.minoAmount == 0

				drawComboMessage(player, cPlayer, #clearedLines, mino.didTspin, didDoAllClear)

				cPlayer.lastLinesCleared = #clearedLines

				-- give the other fucktard(s) some garbage
				cPlayer.garbage = (didDoAllClear and -10 or 0) + cPlayer.garbage - calculateGarbage(#clearedLines, cPlayer.combo, player.backToBack, mino.didTspin)	-- calculate T-spin later
				if cPlayer.garbage < 0 then
					for e, enemy in pairs(game.pp) do
						if e ~= playerNumber then
							enemy.garbage = enemy.garbage - cPlayer.garbage
						end
					end
				end
				cPlayer.garbage = math.max(0, cPlayer.garbage)

				for l = 1, #clearedLines do
					for x = 1, board.xSize do
						board[clearedLines[l]][x][2] = "c"
					end
				end
				-- make the other network player see the flash
				sendInfo("flash_special", false)
				player.flashingSpecial = true
				renderBoard(board, 0, 0, true)
				for i = 1, 0, -0.12 do
					game.setPaletteColor(4096, i,i,i)
					sleep(0.05)
				end
				for i = #clearedLines, 1, -1 do
					table.remove(board, clearedLines[i])
				end
				for i = 1, #clearedLines do
					table.insert(board, 1, false)
				end
				board = clearBoard(board)
				player.flashingSpecial = false
			end

			-- take some garbage for yourself

			if cPlayer.garbage > 0 then
				doleOutGarbage(player, cPlayer, cPlayer.garbage)
			end

			if #clearedLines == 0 and game.running and not cPlayer.didHold then
				mino.draw(false, "c")
				game.setPaletteColor(colors.brown, table.unpack(darkerPalettes[mino.color]))
				renderBoard(board, 0, 0, true)
				sleep(game.appearanceDelay)
				mino.draw(true)
				--board.minoAmount = board.minoAmount + mino.size
				renderBoard(board, 0, 0, false)
			end
		end
	else
		-- if you're a client, take in all that board info and just fukkin draw it

		inputTimer = os.startTimer(game.inputDelay)

		local timeoutTimer = os.startTimer(3)

		while game.running do
			evt = {os.pullEvent()}
			if evt[1] == "new_player_info" then
				player = game.p[game.you]
				for k,v in pairs(game.p) do
					renderBoard(v.board, 0, 0, false)
					renderBoard(v.holdBoard, 0, 0, false)
					renderBoard(v.queueBoard, 0, 0, false)
					drawScore(v, game.pp[k])
					drawLevel(v, game.pp[k])
					term.setCursorPos(13 + v.xmod, 13 + v.ymod)
					term.write("HOLD")
				end
				os.cancelTimer(timeoutTimer or 0)
				timeoutTimer = os.startTimer(3)
			elseif evt[1] == "timer" then
				if evt[2] == inputTimer then
					os.cancelTimer(inputTimer or 0)
					inputTimer = os.startTimer(game.inputDelay)
					if player then
						for k,v in pairs(player.control) do
							player.control[k] = v + game.inputDelay
						end
					end
				elseif evt[2] == timeoutTimer then
					return
				end
			end
		end

	end
end

-- records all key input
local getInput = function()
	local evt
	while true do
		evt = {os.pullEvent()}
		if evt[1] == "key" and evt[3] == false then
			keysDown[evt[2]] = 1
		elseif evt[1] == "key_up" then
			keysDown[evt[2]] = nil
		end
		if (evt[1] == "key" and evt[3] == false) or (evt[1] == "key_up") then
			if game.revControl[evt[2]] then
				game.p[game.you].control[game.revControl[evt[2]]] = keysDown[evt[2]]
				if not game.net.isHost then
					sendInfo("send_info", false)
				end
			end
		end
	end
end

local cTime
local networking = function()
	local evt, side, channel, repchannel, msg, distance
	local currentPlayers = 1
	while true do
		if game.net.useSkynet then
			evt, channel, msg = os.pullEvent("skynet_message")
		else
			evt, side, channel, repchannel, msg, distance = os.pullEvent("modem_message")
		end
		if channel == game.net.channel and type(msg) == "table" then
			if game.net.waitingForGame then
				if type(msg.time) == "number" and msg.command == "find_game" then
					if msg.instanceID ~= game.instanceID then
						if msg.time < cTime then
							game.net.isHost = false
							game.you = 2
							game.net.gameID = msg.gameID
						else
							game.net.isHost = true
						end

						transmit({
							gameID = game.net.gameID,
							time = cTime,
							command = "find_game",
							instanceID = game.instanceID
						})
						game.net.waitingForGame = false
						os.queueEvent("new_game", game.net.gameID)
						return game.net.gameID
					end
				end
			else
				if msg.gameID == game.net.gameID then

					if game.net.isHost then
						if type(msg.control) == "table" then
							if type(msg.you) == "number" and msg.you ~= game.you then
								game.p[msg.you].control = msg.control
								for y = 1, game.p[msg.you].board.ySize do
									for x = 1, game.p[msg.you].board.xSize do
										ageSpace(game.p[msg.you].board, x, y)
									end
								end
							end
						end
					else
						if type(msg.you) == "number" and msg.you ~= game.you then
							if type(msg.p) == "table" then
								if msg.pNum then
									for k,v in pairs(msg.p) do
										if k ~= "control" then
											game.p[msg.pNum][k] = v
										end
									end
								else
									game.p = msg.p
								end
								if msg.specialColor then
									game.setPaletteColor(tColors.special, table.unpack(msg.specialColor))
								end
								os.queueEvent("new_player_info", msg.p)
							end
							if msg.command == "quit_game" then
								return
							end
							if msg.command == "flash_special" then
								for i = 1, 0, -0.12 do
									game.setPaletteColor(4096, i,i,i)
									renderBoard(game.p[msg.you].board, 0, 0, true)
									sleep(0.05)
								end
							end
						end
					end

				end
			end
		end
	end
end

local cwrite = function(text, y, xdiff, wordPosCheck)
	wordPosCheck = wordPosCheck or #text
	term.setCursorPos(math.floor(scr_x / 2 - math.floor(0.5 + #text + (xdiff or 0)) / 2), y or (scr_y - 2))
	term.write(text)
	return (scr_x / 2) - (#text / 2) + wordPosCheck
end

local setUpModem = function()
	if game.net.useSkynet then
		if fs.exists(game.net.skynetPath) then
			game.net.skynet = dofile(game.net.skynetPath)
			term.clear()
			cwrite("Connecting to Skynet...", scr_y / 2)
			game.net.skynet.open(game.net.channel)
			return true
		else
			term.clear()
			cwrite("Downloading Skynet...", scr_y / 2)
			local prog = http.get(game.net.skynetURL)
			if prog then
				local file = fs.open(game.net.skynetPath, "w")
				file.write(prog.readAll())
				file.close()
				skynet = dofile(game.net.skynetPath)
				cwrite("Connecting to Skynet...", 1 + scr_y / 2)
				skynet.open(game.net.channel)
				return true
			else
				return false, "Could not download Skynet."
			end
		end
	else
		-- unload / close skynet
		if game.net.skynet then
			if game.net.skynet.socket then
				game.net.skynet.socket.close()
			end
			game.net.skynet = nil
		end
		game.net.modem = peripheral.find("modem")
		if (not game.net.modem) and ccemux then
			ccemux.attach("top", "wireless_modem")
			game.net.modem = peripheral.find("modem")
		end
		if game.net.modem then
			game.net.modem.open(game.net.channel)
			return true
		else
			return false, "No modem was found."
		end
	end
end

local pleaseWait = function()
	local periods = 1
	local maxPeriods = 5
	term.setBackgroundColor(tColors.black)
	term.setTextColor(tColors.gray)
	term.clear()

	local tID = os.startTimer(0.2)
	local evt, txt
	if game.net.useSkynet then
		txt = "Waiting for Skynet game"
	else
		txt = "Waiting for modem game"
	end

	while true do
		cwrite("(Press 'Q' to cancel)", 2)
		cwrite(txt, scr_y - 2, maxPeriods)
		term.write(("."):rep(periods))
		evt = {os.pullEvent()}
		if evt[1] == "timer" and evt[2] == tID then
			tID = os.startTimer(0.5)
			periods = (periods % maxPeriods) + 1
			term.clearLine()
		elseif evt[1] == "key" and evt[2] == keys.q then
            return
        end
	end
end

local titleScreen = function()	-- mondo placeholder
	term.setTextColor(tColors.white)
	term.setBackgroundColor(tColors.black)
	term.clear()
	cwrite("LDris", 3)
	cwrite("by LDDestroier", 5)
	cwrite("Press 1 to play a game.", 7)
	if game.net.useSkynet then
		cwrite("Press 2 to play an HTTP game.", 8)
		cwrite("Press S to disable Skynet.", 9)
	else
		cwrite("Press 2 to play a modem game.", 8)
		cwrite("Press S to enable Skynet.", 9)
	end
	cwrite("Press H to see controls.", 10)
	cwrite("Press Q to quit.", 11)
	local evt
	while true do
		evt = {os.pullEvent()}
		if evt[1] == "key" then
			if evt[2] == keys.one then
				return "1P"
			elseif evt[2] == keys.two then
				return "2P"
			elseif evt[2] == keys.s then
				return "skynet"
			elseif evt[2] == keys.h then
				return "help"
			elseif evt[2] == keys.q then
				return "quit"
			end
		end
	end
end

local screenError = function(...)
	local lines = {...}
	term.setBackgroundColor(tColors.black)
	term.setTextColor(tColors.white)
	term.clear()
	for i = 1, #lines do
		cwrite(lines[i], 2 + i)
	end
	cwrite("Press any key to continue.", #lines + 4)
	sleep(0)
	repeat until os.pullEvent("key")
end

-- a lot of these menus and whatnot are very primitive. I can improve that later
local showHelp = function()
	term.setBackgroundColor(tColors.black)
	term.setTextColor(tColors.white)
	term.clear()
	cwrite("CONTROLS (defaults):", 2)
	term.setCursorPos(1, 4)
	print(" Move Piece:   LEFT and RIGHT")
	print(" Hard Drop:    UP")
	print(" Soft Drop:    DOWN")
	print(" Sonic Drop:   SPACE")
	print(" Rotate Piece: Z and X")
	print(" Hold Piece:   L.SHIFT")
	print(" Quit:         Q")
	print("\n Press any key to continue.")
	sleep(0)
	repeat until os.pullEvent("key")
end

local main = function()

	local rVal		-- please wait result
	local modeVal	-- title screen mode result
	local funcs

	setUpModem()

	while true do

		game.you = 1
		game.net.isHost = true
		finished = false
		game.running = true

		while true do
			modeVal = titleScreen()

			if modeVal == "1P" then
				game.net.active = false
				game.amountOfPlayers = 1
				game.gameDelay = 0.05
				break
			elseif modeVal == "2P" then
				if setUpModem() then
					if (game.net.skynet and game.net.useSkynet) then
						game.gameDelay = 0.1
					else
						game.gameDelay = 0.05
					end
					game.net.active = true
					game.amountOfPlayers = 2
					break
				else
					screenError("A modem is required for multiplayer.")
					finished = true
				end
			elseif modeVal == "skynet" then
				if http.websocket then
					game.net.useSkynet = not game.net.useSkynet
					setUpModem()
				else
					screenError(
						"Skynet requires websocket support.",
						"Use CCEmuX, CC:Tweaked,",
						"or CraftOS-PC 2.2 or higher to play."
					)
					finished = true
				end
			elseif modeVal == "help" then
				showHelp()
			elseif modeVal == "quit" then
				return false
			end
		end

		if game.net.active then

			game.net.waitingForGame = true

			cTime = getTime()
			transmit({
				gameID = game.net.gameID,
				time = cTime,
				command = "find_game",
				instanceID = game.instanceID
			})
			if game.net.useSkynet then
				rVal = parallel.waitForAny( networking, pleaseWait, game.net.skynet.listen )
			else
				rVal = parallel.waitForAny( networking, pleaseWait )
			end
			sleep(0.1)

			if rVal == 1 then

				funcs = {
					getInput,
				}

				if game.net.active then
					table.insert(funcs, networking)
					if game.net.useSkynet and game.net.skynet then
						table.insert(funcs, game.net.skynet.listen)
					end
				end

				initializePlayers(game.amountOfPlayers or 1)

				if game.net.isHost then
					for k,v in pairs(game.p) do
						funcs[#funcs + 1] = function()
							return startGame(k)
						end
					end
				else
					funcs[#funcs + 1] = startGame
				end

			else
				finished = true
			end

		else

			funcs = {getInput}
			initializePlayers(game.amountOfPlayers or 1)

			for k,v in pairs(game.p) do
				funcs[#funcs + 1] = function()
					return startGame(k)
				end
			end

		end

		if not finished then

			term.setBackgroundColor(tColors.gray)
			term.clear()

			parallel.waitForAny(table.unpack(funcs))

		end

	end

end

main()

-- reset palette to back from whence it came
for k,v in pairs(colors) do
	if type(v) == "number" then
		term.setPaletteColor(v, term.nativePaletteColor(v))
	end
end

term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)

if game.net.skynet then
	if game.net.skynet.socket then
		game.net.skynet.socket.close()
	end
end

for i = 1, 5 do
	term.scroll(1)
	if i == 3 then
		term.setCursorPos(1, scr_y)
		term.write("Thanks for playing!")
	end
	sleep(0.05)
end
term.setCursorPos(1, scr_y)