--[[
	TRON Light Cycle Game
	programmed by LDDestroier
	
	wget https://raw.githubusercontent.com/LDDestroier/CC/master/tron
--]]

local port = 701

local scr_x, scr_y = term.getSize()
local modem = peripheral.find("modem")
if (not modem) and ccemux then
	ccemux.attach("top", "wireless_modem")
	modem = peripheral.find("modem")
end

if modem then
	modem.open(port)
else
	error("You should attach a modem.")
end

local gamename = ""
local isHost
local squareGrid = true

local waitingForGame = true

local deepCopy
deepCopy = function(tbl, ...)
	local output = {}
	for k,v in pairs(tbl) do
		if type(v) == "table" then
			output[k] = deepCopy(v)
		else
			output[k] = v
		end
	end
	for i = 1, #arg do
		output[#output+1] = arg[i]
	end
	return output
end

local initGrid = {
	x1 = -100,
	y1 = -100,
	x2 = 100,
	y2 = 100,
	border = "#",
	voidcol = "f",
	forecol = "8",
	backcol = "7",
	edgecol = "0"
}
grid = deepCopy(initGrid)

local you = 1
local nou = 2

local keysDown = {}
local netKeysDown = {}

-- the scrolling of the screen
local scrollX = 0
local scrollY = 0

-- used when panning with WASD
local scrollAdjX = 0
local scrollAdjY = 0

local lockInput = false
local gameDelayInit = 0.1 -- lower value = faster game. I'd reccommend 0.1 for SMP play.
local player

local resetPlayers = function()
	player = {
		[1] = {
			x = -2,
			y = -5,
			direction = -1,
			char = "@",
			color = {
				colors.blue,
				colors.blue,
				colors.blue,
				colors.cyan,
				colors.cyan,
				colors.lightBlue,
				colors.lightBlue,
				colors.cyan,
				colors.cyan
			},
			dead = false,
			putTrail = true
		},
		[2] = {
			x = 2,
			y = -5,
			direction = -1,
			char = "@",
			color = {
				colors.red,
				colors.red,
				colors.red,
				colors.orange,
				colors.orange,
				colors.yellow,
				colors.yellow,
				colors.orange,
				colors.orange
			},
			dead = false,
			putTrail = true
		}
	}
end

resetPlayers()

local images = {
	logo = {
		{
			" •ƒƒƒƒƒƒƒƒƒ•—ƒƒƒƒƒƒƒ‹‹   ‡‡ƒƒƒ‹‹  Ÿ‹    •ƒƒ•",
			" •ƒƒƒ”€—ƒƒƒ•‚ƒƒƒƒƒ‹€€€Š —€Ÿƒƒƒ€” •‚‚  •€€•",
			"     •€•          ‚‚ƒƒ•€€—€€€”€€••€€‹‹ •€€•",
			"     •€•    —ƒ”‹“ƒƒ‹€€€•€€•€€€•€€••€•ˆƒ€€•",
			"     •€•    •€• ‚‹€€‹€Š€‹‡€Ÿ…•€•  ‚‚€•",
			"     •€•    •€•   ‹€‚‹ ‹‹€€€Ÿ‡‡ •€•    ‹‹•",
			"             Š  ‚‹‡       ‚…",
		},
		{
			" f7777777777777777777f   f77777f  7f    f777",
			" f99979999979999999999f 799999799 77f7  f997",
			"     799          79999f997ffff9977997f f997",
			"     799    7797777fffff997ffff9977997797997",
			"     799    799 799977f7797fff7997799  79797",
			"     799    799   7797f 797999997 799    797",
			"     777    777    7777  7777777  777     77",
		},
		{
			" 7999999999f9999999997   7999997  97    799f",
			" 7777997777f77777779997 997777997 997f  799f",
			"     997          f7777799ffff799f99997 799f",
			"     997    997f9997fff799ffff799f997ff7999f",
			"     997    997 f7999fff999777997f997  f799f",
			"     997    997   f9997 f7999977f 997    f7f",
			"     fff    fff    ffff  fffffff  fff     ff",
		}
	},
	win = {
		{
			"€•€€€€€€€••€€€€€€€€Š€€€€•€€€•",
			"€•€€€€€€€•‚€€•€ƒ€€€‚€€•€€€•",
			"€•€€‡€€€•€€€€•€€€€•‹€‹€•€€€•",
			"€•ŸŸ€‹€€•€€€€•€€€€•€‚‚…€€€•",
			"€‚€‡€‚‚€•Ÿ€€•€€€•€€€‹€€€",
			"€Ÿ€€€€‹€••€€€€€€€€•€€€€•€€€•",
		},
		{
			"55ffffff55f555555f5ffffff5f55",
			"55ffffff5555f55f5f55f5fff5f55",
			"55fff5ff55fff55fff5555fff5f55",
			"55ff55ff55fff55fff55f5fff5f55",
			"5f55f5ff55f5f55fff55fff555ff5",
			"555ffff555f555555f55fffff5f55",
		},
		{
			"5fffffff5f5555555f55ffff55f5f",
			"5fffffff5ffff5ffff555fff55f5f",
			"5fff5fff5ffff5ffff5ff55f55f5f",
			"5f55f55f5ffff5ffff5fff5555f5f",
			"555fff555f5ff5ff5f5fffff55f5f",
			"5fffffff5f5555555f5fffff55f5f",
		}
	},
	lose = {
		{
			"€•€€€€€€Ÿ€€€‚€€€€€€‚€€€€€€€€",
			"€•€€€€€€€Ÿ€‚€€€—€€€‚ƒ€€•€€€‚ƒ",
			"€•€€€€€€€•€€€•€€€€ƒƒƒƒ‹€€‚ƒƒƒ”€",
			"€•€€€€€€€•€€€•€€‚ƒƒƒƒ€€€—ƒƒƒ€",
			"€•€€€€€€€‚€Ÿ€€€€€…€€€•€€€Ÿ",
			"€€€€€€€€‚€€€Ÿ€€€€€€Ÿ€€€€€€€€",
		},
		{
			"eeffffffffeeefeffeeeeeffeeeeeee",
			"eeffffffeeefefefeefffeefeefffee",
			"eeffffffeeffffefeeffffffeffffef",
			"eeffffffeeffffefeeeeefefeeeeeef",
			"eeffffffefefffeffeffffefeefffff",
			"eeeeeeefefeeeeeffeeeeeefeeeeeee",
		},
		{
			"efffffffeeeeeeffeeeeeeefeeeeeee",
			"efffffffeffffeefefffffffeffffff",
			"efffffffeffffeefeeeeeeefeeeeeff",
			"efffffffeffffeeffffffeefeffffff",
			"efffffffeeffeeefeffffeefeffffee",
			"eeeeeeeffeeeefffeeeeeeffeeeeeee",
		}
	},
	tie = {
		{
			"€€€€€€€••€€€€€€€€€€€€€€€",
			"€€€€•€€€‚€•€€€ƒ€€•€€€€ƒ",
			"€€€€•€€€€€€•€€€€€€‚ƒƒƒ”€",
			"€€€€•€€€€€€•€€€€€€—ƒƒƒ€",
			"€€€€•€€€Ÿ€•€€€€€•€€€€",
			"€€€€•€€€•€€€€€€€€€€€€€€€",
		},
		{
			"77888800f0000000f0888877",
			"fff88fff00ff0ff0f08ffff7",
			"fff88fffffff0ffff0ffff7f",
			"fff88fffffff0ffff088887f",
			"fff88ffff0ff0ffff08fffff",
			"fff88ffff0000000f0888877",
		},
		{
			"7788880f00000000f0888877",
			"fff8fffffff00ffff0ffffff",
			"fff8fffffff00ffff08888ff",
			"fff8fffffff00ffff0ffffff",
			"fff8ffff0ff00ff0f0fffff7",
			"fff8ffff00000000f0888877",
		},
	}
}
for k,v in pairs(images) do
	v.x = #v[1][1]
	v.y = #v[1]
end

local drawImage = function(im, x, y)
	local cx, cy = term.getCursorPos()
	for iy = 1, #im[1] do
		term.setCursorPos(x,y+(iy-1))
		term.blit(im[1][iy], im[2][iy], im[3][iy])
	end
	term.setCursorPos(cx,cy)
end

local deadGuys = {}
local trail = {}

local putTrail = function(p)
	trail[p.y] = trail[p.y] or {}
	trail[p.y][p.x] = {
		player = p,
		age = 0
	}
end

local getTrail = function(x, y)
	if trail[y] then
		if trail[y][x] then
			if doAge then
				trail[y][x].age = trail[y][x].age + 1
			end
			return trail[y][x].player.char, trail[y][x].player.color, trail[y][x].age
		end
	end
	return false
end

local ageTrails = function()
	for y,l in pairs(trail) do
		for x,v in pairs(l) do
			trail[y][x].age = trail[y][x].age + 1
		end
	end
end

local toblit = {
	[0] = " ",
	[colors.white] = "0",
	[colors.orange] = "1",
	[colors.magenta] = "2",
	[colors.lightBlue] = "3",
	[colors.yellow] = "4",
	[colors.lime] = "5",
	[colors.pink] = "6",
	[colors.gray] = "7",
	[colors.lightGray] = "8",
	[colors.cyan] = "9",
	[colors.purple] = "a",
	[colors.blue] = "b",
	[colors.brown] = "c",
	[colors.green] = "d",
	[colors.red] = "e",
	[colors.black] = "f"
}

local control = {
	up = keys.up,
	down = keys.down,
	left = keys.left,
	right = keys.right,
	lookUp = keys.w,
	lookDown = keys.s,
	lookLeft = keys.a,
	lookRight = keys.d,
	release = keys.space
}

-- keeps track of where you are
local gamemode = ""


local gridFore, gridBack
if squareGrid then
	gridFore = {
		"+-------",
		"|       ",
		"|       ",
		"|       ",
		"|       "
	}
	gridBack = {
		"+------------",
		"|            ",
		"|            ",
		"|            ",
		"|            ",
		"|            ",
		"|            ",
		"|            "
	}
else
	gridFore = {
		"    /      ",
		"   /       ",
		"  /        ",
		" /         ",
		"/__________"
	}
	gridBack = {
		"       /        ",
		"      /         ",
		"     /          ",
		"    /           ",
		"   /            ",
		"  /             ",
		" /              ",
		"/_______________"
	}
end

local dirArrow = {
	[-1] = "^",
	[0] = ">",
	[1] = "V",
	[2] = "<"
}

local doesIntersectBorder = function(x, y)
	return x == grid.x1 or x == grid.x2 or y == grid.y1 or y == grid.y2
end

--draws grid and background at scroll 'x' and 'y'
local drawGrid = function(x, y)
	x, y = math.floor(x + 0.5), math.floor(y + 0.5)
	local bg = {{},{},{}}
	local foreX, foreY
	local backX, backY
	local adjX, adjY
	local trailChar, trailColor, trailAge, isPlayer
	for sy = 1, scr_y do
		bg[1][sy] = ""
		bg[2][sy] = ""
		bg[3][sy] = ""
		for sx = 1, scr_x do
			adjX = (sx + x)
			adjY = (sy + y)
			foreX = 1 + (sx + x) % #gridFore[1]
			foreY = 1 + (sy + y) % #gridFore
			backX = 1 + math.floor(sx + (x / 2)) % #gridBack[1]
			backY = 1 + math.floor(sy + (y / 2)) % #gridBack
			trailChar, trailColor, trailAge = getTrail(adjX, adjY)
			isPlayer = false
			for i = 1, #player do
				if player[i].x == adjX and player[i].y == adjY then
					isPlayer = i
					break
				end
			end
			if isPlayer and not (doesIntersectBorder(adjX, adjY)) then
				bg[1][sy] = bg[1][sy] .. dirArrow[player[isPlayer].direction]
				bg[2][sy] = bg[2][sy] .. toblit[player[isPlayer].color[1]]
				bg[3][sy] = bg[3][sy] .. grid.voidcol
			else
				if trailChar and trailColor then
					trailColor = trailColor[1 + ((trailAge - 1) % #trailColor)]
					bg[1][sy] = bg[1][sy] .. trailChar
					bg[2][sy] = bg[2][sy] .. toblit[trailColor]
					bg[3][sy] = bg[3][sy] .. grid.voidcol
				else
					if adjX < grid.x1 or adjX > grid.x2 or adjY < grid.y1 or adjY > grid.y2 then
						bg[1][sy] = bg[1][sy] .. " "
						bg[2][sy] = bg[2][sy] .. grid.voidcol
						bg[3][sy] = bg[3][sy] .. grid.voidcol
					elseif doesIntersectBorder(adjX, adjY) then
						bg[1][sy] = bg[1][sy] .. grid.border
						bg[2][sy] = bg[2][sy] .. grid.voidcol
						bg[3][sy] = bg[3][sy] .. grid.edgecol
					else
						if gridFore[foreY]:sub(foreX,foreX) ~= " " then
							bg[1][sy] = bg[1][sy] .. gridFore[foreY]:sub(foreX,foreX)
							bg[2][sy] = bg[2][sy] .. grid.forecol
							bg[3][sy] = bg[3][sy] .. grid.voidcol
						elseif gridBack[backY]:sub(backX,backX) ~= " " then
							bg[1][sy] = bg[1][sy] .. gridBack[backY]:sub(backX,backX)
							bg[2][sy] = bg[2][sy] .. grid.backcol
							bg[3][sy] = bg[3][sy] .. grid.voidcol
						else
							bg[1][sy] = bg[1][sy] .. " "
							bg[2][sy] = bg[2][sy] .. grid.voidcol
							bg[3][sy] = bg[3][sy] .. grid.voidcol
						end
					end
				end
			end
		end
	end
	for sy = 1, scr_y do
		term.setCursorPos(1,sy)
		term.blit(bg[1][sy], bg[2][sy], bg[3][sy])
	end
end

local render = function()
	local p = player[you]
	drawGrid(scrollX + scrollAdjX, scrollY + scrollAdjY)
	term.setCursorPos(1,1)
	term.setTextColor(player[you].color[1])
	term.write("P" .. you)
end

local pleaseWait = function()
	local periods = 1
	local maxPeriods = 5
	term.setBackgroundColor(colors.black)
	term.setTextColor(colors.gray)
	term.clear()
	
	local tID = os.startTimer(0.2)	
	local evt
	local txt = "Waiting for game"
	
	while true do
		term.setCursorPos(math.floor(scr_x / 2 - (#txt + maxPeriods) / 2), scr_y - 2)
		term.write(txt .. ("."):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] == "new_game" then
			return evt[2]
		end
	end
end

local startCountdown = function()
	local cName = "PLAYER " .. you
	local col = colors.white
	for k,v in pairs(colors) do
		if player[you].color[1] == v then
			cName = k:upper()
			col = v
			break
		end
	end
	local cMessage = "You are "
	scrollX = player[you].x - math.floor(scr_x / 2)
	scrollY = player[you].y - math.floor(scr_y / 2)
	for i = 3, 1, -1 do
		render()
		term.setCursorPos(math.floor(scr_x / 2 - (#cMessage + #cName) / 2), math.floor(scr_y / 2) + 2)
		term.setTextColor(colors.white)
		term.write(cMessage)
		term.setTextColor(col)
		term.write(cName)
		term.setTextColor(colors.white)
		term.setCursorPos(math.floor(scr_x / 2 - 2), math.floor(scr_y / 2) + 4)
		term.write(i .. "...")
		sleep(1)
	end
end

local makeMenu = function(x, y, options)
	local cpos = 1
	local cursor = "> "
	local rend = function()
		for i = 1, #options do
			term.setCursorPos(x, y + (i - 1))
			if i == cpos then
				term.setTextColor(colors.white)
				term.write(cursor .. options[i])
			else
				term.setTextColor(colors.gray)
				term.write((" "):rep(#cursor) .. options[i])
			end
		end
	end
	local evt
	while true do
		rend()
		evt = {os.pullEvent()}
		if evt[1] == "key" then
			if evt[2] == keys.up then
				cpos = (cpos - 2) % #options + 1
			elseif evt[2] == keys.down then
				cpos = (cpos % #options) + 1
			elseif evt[2] == keys.enter then
				return cpos
			end
		end
	end
end

local titleScreen = function()
	term.clear()
	drawImage(images.logo, math.ceil(scr_x / 2 - images.logo.x / 2), 2)
	
	local choice = makeMenu(2, scr_y - 4, {
		"Start Game",
		"How to Play",
		"Grid Demo",
		"Exit"
	})
	if choice == 1 then
		return "start"
	elseif choice == 2 then
		return "help"
	elseif choice == 3 then
		return "demo"
	elseif choice == 4 then
		return "exit"
	end
end

local cleanExit = function()
	term.setBackgroundColor(colors.black)
	term.setTextColor(colors.white)
	term.clear()
	term.setCursorPos(1,1)
	print("Thanks for playing!")
end

local getInput = function()
	local evt
	while true do
		evt = {os.pullEvent()}
		if lockInput then
			keysDown = {}
		else
			if evt[1] == "key" then
				keysDown[evt[2]] = true
			elseif evt[1] == "key_up" then
				keysDown[evt[2]] = false
			end
		end
	end
end

local scrollToPosition = function(x, y)
	for i = 1, 16 do
		scrollX = (scrollX + x - (scr_x/2)) / 2
		scrollY = (scrollY + y - (scr_y/2)) / 2
		render()
		sleep(0.05)
	end
end

local gridDemo = function()
	keysDown = {}
	while true do
		if keysDown[keys.left] then
			scrollX = scrollX - 1
		end
		if keysDown[keys.right] then
			scrollX = scrollX + 1
		end
		if keysDown[keys.up] then
			scrollY = scrollY - 1
		end
		if keysDown[keys.down] then
			scrollY = scrollY + 1
		end
		if keysDown[keys.q] then
			return "end"
		end
		drawGrid(scrollX, scrollY)
		ageTrails()
		sleep(gameDelay)
	end
end

local sendInfo = function(gameID)
	modem.transmit(port, port, {
		player = player,
		gameID = gameID,
		keysDown = keysDown,
		trail = isHost and trail or nil,
		deadGuys = isHost and deadGuys or {},
	})
end

local waitForKey = function(time)
	sleep(time or 0.1)
	os.pullEvent("key")
end

local deadAnimation = function(doSend)
	for k,v in pairs(deadGuys) do
		player[k].char = "X"
		lockInput = true
	end
	if doSend then
		sendInfo(gamename)
	end
	if deadGuys[you] or deadGuys[nou] then
		term.setTextColor(colors.white)
		if deadGuys[you] and deadGuys[nou] then
			scrollToPosition(player[nou].x, player[nou].y)
			scrollToPosition(player[you].x, player[you].y)
			drawImage(images.tie, math.ceil(scr_x / 2 - images.tie.x / 2), math.floor(scr_y / 2 - images.tie.y / 2))
			waitForKey(1)
			return "end"
		else
			if deadGuys[you] then
				scrollX, scrollY = player[nou].x - scr_x / 2, player[nou].y - scr_y / 2
				scrollToPosition(player[you].x, player[you].y)
				drawImage(images.lose, math.ceil(scr_x / 2 - images.lose.x / 2), math.floor(scr_y / 2 - images.lose.y / 2))
				waitForKey(1)
				return "end"
			elseif deadGuys[nou] then
				scrollToPosition(player[nou].x, player[nou].y)
				drawImage(images.win, math.ceil(scr_x / 2 - images.win.x / 2), math.floor(scr_y / 2 - images.win.y / 2))
				waitForKey(1)
				return "end"
			end
		end
	end
end

local moveTick = function(doSend)
	local p
	for i = 1, #player do
		p = player[i]
		if not p.dead then
			if isHost then
				p.x = p.x + math.floor(math.cos(math.rad(p.direction * 90)))
				p.y = p.y + math.floor(math.sin(math.rad(p.direction * 90)))
			end
		end
		if isHost then
			if p.putTrail then
				putTrail(p)
			end
		end
	end
	for i = 1, #player do
		p = player[i]
		if getTrail(p.x, p.y) or doesIntersectBorder(p.x, p.y) then
			p.dead = true
			deadGuys[i] = true
		end
	end
	ageTrails()
	return deadAnimation(doSend)
end

local setDirection = function(keylist, p, checkDir)
	p.putTrail = not keylist[control.release]
	if keylist[control.left] and (checkDir or p.direction) ~= 0 then
		p.direction = 2
	elseif keylist[control.right] and (checkDir or p.direction) ~= 2 then
		p.direction = 0
	elseif keylist[control.up] and (checkDir or p.direction) ~= 1 then
		p.direction = -1
	elseif keylist[control.down] and (checkDir or p.direction) ~= -1 then
		p.direction = 1
	end
end

local game = function()
	local outcome
	local p, np
	while true do
		p = player[you]
		np = player[nou]
		
		if isHost then
			setDirection(keysDown, p)
			setDirection(netKeysDown, np)
		else
			setDirection(keysDown, p)
		end
		
		if keysDown[control.lookLeft] then
			scrollAdjX = scrollAdjX - 2
		end
		if keysDown[control.lookRight] then
			scrollAdjX = scrollAdjX + 2
		end
		if keysDown[control.lookUp] then
			scrollAdjY = scrollAdjY - 1.5
		end
		if keysDown[control.lookDown] then
			scrollAdjY = scrollAdjY + 1.5
		end
		
		scrollAdjX = scrollAdjX * 0.8
		scrollAdjY = scrollAdjY * 0.8

		if isHost then
			outcome = moveTick(true)
		else
			outcome = deadAnimation(true)
		end
		
		if outcome == "end" then
			return
		else
			scrollX = p.x - math.floor(scr_x / 2)
			scrollY = p.y - math.floor(scr_y / 2)

			render()
			sleep(gameDelay)
		end
	end
end

local decision

local networking = function()
	local evt, side, channel, repchannel, msg, distance
	while true do
		evt, side, channel, repchannel, msg, distance = os.pullEvent("modem_message")
		if channel == port and repchannel == port and type(msg) == "table" then
			if type(msg.player) == "table" and type(msg.gameID) == "string" then
				
				if waitingForGame and (type(msg.new) == "number") then
					if msg.new < os.time() then
						gamename = msg.gameID
						isHost = false
						gameDelay = tonumber(msg.gameDelay) or 0.05
						grid = msg.grid or deepCopy(initGrid)
					else
						you, nou = nou, you
						isHost = true
					end
					you, nou = nou, you
					modem.transmit(port, port, {
						player = player,
						gameID = gamename,
						new = isHost and (-math.huge) or (math.huge),
						grid = initGrid
					})
					waitingForGame = false
					netKeysDown = {}
					os.queueEvent("new_game", gameID)
					
				elseif msg.gameID == gamename then
					if not isHost then
						player = msg.player
						trail = msg.trail
						deadGuys = msg.deadGuys
					elseif type(msg.keysDown) == "table" then
						netKeysDown = msg.keysDown
					end
				end
				
			end
		end
	end
end

local helpScreen = function()
	term.setBackgroundColor(colors.black)
	term.setTextColor(colors.white)
	term.clear()
	term.setCursorPos(1,2)
	print([[
		Move with arrow keys.
		Pan the camera with WASD.
		Hold SPACE to create gaps.
		
		That's basically it.
		Press any key to go back.
	]])
	waitForKey(0.25)
end

while true do
	decision = titleScreen()
	lockInput = false
	if decision == "start" then
		trail = {}
		deadGuys = {}
		gameDelay = gameDelayInit
		grid = deepCopy(initGrid)
		resetPlayers()
		you, nou = 1, 2
		gamename = ""
		for i = 1, 32 do
			gamename = gamename .. string.char(math.random(1,126))
		end
		waitingForGame = true
		modem.transmit(port, port, {
			player = player,
			gameID = gamename,
			new = os.time(),
			gameDelay = gameDelayInit,
			grid = initGrid
		})
		parallel.waitForAny(pleaseWait, networking)
		sleep(0.1)
		startCountdown()
		parallel.waitForAny(getInput, game, networking)
	elseif decision == "help" then
		helpScreen()
	elseif decision == "demo" then
		parallel.waitForAny(getInput, gridDemo)
	elseif decision == "exit" then
		return cleanExit()
	end
end