-- ELECTRIS
-- Pineapple Productions
-- Copyright 2006 Andrew Carpenter

-- CONSTANTS
PENDING = 1
INVALID = 2
VALID = 3
RED = Color.new(220,0,0)
BLACK = Color.new(0,0,0)

-- FUNCTIONS

function loadData()
	img_ready = Image.load("graphics/ready.png")
	img_bg = Image.load("graphics/background.png")
	img_block = Image.load("graphics/block.png")
	img_eraser = Image.load("graphics/eraser.png")
	img_text = Image.load("graphics/text.png")
	
	snd_drop = Sound.load("sound/click2.wav")
	snd_remove = Sound.load("sound/longzap.wav")
	snd_bonus = Sound.load("sound/doorbell.wav")
	snd_gameover = Sound.load("sound/gameover.wav")
	snd_levelup = Sound.load("sound/beep.wav")
	snd_alarm = Sound.load("sound/alarm.wav")

	-- block definition: tile,N,E,S,W
	block = {}
	block[1] = { [1]={1,n=false,e=false,s=false,w=false} }
	
	block[2] = { [1]={2,n=true,e=false,s=false,w=false},
                     [2]={3,n=false,e=true,s=false,w=false},
                     [3]={4,n=false,e=false,s=true,w=false},
                     [4]={5,n=false,e=false,s=false,w=true} }
		     
	block[3] = { [1]={6,n=true,e=true,s=false,w=false},
                     [2]={7,n=false,e=true,s=true,w=false},
                     [3]={8,n=false,e=false,s=true,w=true},
                     [4]={9,n=true,e=false,s=false,w=true} }
		     
	block[4] = { [1]={10,n=true,e=true,s=true,w=false},
                     [2]={11,n=false,e=true,s=true,w=true},
                     [3]={12,n=true,e=false,s=true,w=true},
                     [4]={13,n=true,e=true,s=false,w=true} }
		     
	block[5] = { [1]={14,n=true,e=false,s=true,w=false},
                     [2]={15,n=false,e=true,s=false,w=true} }
		     
	block[6] = { [1]={16,n=true,e=true,s=true,w=true} }

	-- eraser definition: tile, S,W,E (# to erase)
	block[7] = { [1]={1,s=1,w=0,e=0} }                                            -- single eraser
	block[8] = { [1]={2,s=2,w=0,e=0}, [2]={3,s=1,w=0,e=1}, [3]={4,s=1,w=1,e=0} }  -- double eraser
	block[9] = { [1]={5,s=3,w=0,e=0}, [2]={6,s=1,w=1,e=1} }                       -- triple eraser
	
	-- animation sequence for removing blocks
	sequence = { 3,2,3,2,3,2,3,2,4,5,6,7,8 }
	-- sounds to play at given animation frames
	animsound = { [1]=snd_close, [9]=snd_remove }

	multiplier = 10  -- circuit block count * multiplier = score
	
	emin = { 5, 10, 15 } -- minimum block in circuit to score erasers
	ebonus = 20 -- bonus for not using an eraser (ie dropping on the floor)
	
	loaded = true
	
	levelup = 100 -- score increment at which level increases
	
	penalty = 1800 -- frames without a circuit until penalty rows added (1/60th sec)
end

function gameInit()
	if not loaded then
		loadData()
	end

	--randomize
	math.randomseed( os.time() )
	math.random(); math.random(); math.random()

	-- get current and next blocks
	currentblock = randomBlock()
	pos = { x = 6, y = 0 }
	nextblock = randomBlock()

	-- initialise game grid
	grid = initGrid()
	checkgrid = initGrid()
	
	-- game speed (1/60 sec; lower is faster)
	droptimer = 0
	dropdelay = 60
	
	score = 0
	circuitcount = 0
	level = 1
	chain = 1
	
	penaltyCount = penalty
end	

function loadHelp()
	img_help = {}
	currenthelp = 1;
	
	for count = 1,10 do
	img_help[count] = Image.load("help/".. count ..".png")
	end
end

-- Generate a random block
   -- tile: block tile
   -- rot:  block rotation
function randomBlock()
	
	-- simple tile selection:
	-- local t = math.random(2,6)
	
	-- balanced tile selection:
	local rnd = math.random(1,100)
	
	if     rnd > 80 then t = 2  -- 20%
	elseif rnd > 55 then t = 3  -- 25%
	elseif rnd > 30 then t = 4  -- 25%
	elseif rnd > 10 then t = 5  -- 20%
	else                 t = 6  -- 10%
	end
	
	local r = math.random(1,table.getn(block[t]))
	
	return { tile = t, rot = r };
end

function penaltyBlock()
	return { tile = 1, rot = 1 }
end

function nextBlock()
	currentblock = nextblock
	pos.x = 6; pos.y = 0
	nextblock = randomBlock()
end

function rotateBlock(b)
	local maxrot = table.getn(block[b.tile])
	b.rot = b.rot +1
	if b.rot > maxrot then
		b.rot = 1
	end
end

function getBlock(b)
	return block[ b.tile ][b.rot]
end

function getBlockIndex(b)
	return block[ b.tile ][ b.rot ][1]
end

function drawBlock(x,y,b,i)
	if b.tile >6 then
		screen:blit( 448-(y*26), (x-1)*26 + 6, img_eraser,208-(i*26),(getBlockIndex(b)-1)*26,26,26,true)
	else
		screen:blit( 448-(y*26), (x-1)*26 + 6, img_block,208-(i*26),(getBlockIndex(b)-1)*26,26,26,true)
	end
end

function nextEraser(etype)
	currentblock = { tile = 6 + etype, rot = 1 }
	pos.x = 6; pos.y = 0
end

function isEraser()
	if currentblock.tile > 6 then return true end
	return false
end

function eraseBlocks(x,y,b)
	local block = getBlock(b)
	checkgrid = initGrid()
	checkgrid[x][y] = VALID
	for i=1,block.e do
		if x+i <=10 and y+1 <=17 then checkgrid[x+i][y+1] = VALID end
	end
	for i=1,block.w do
		if x-i >0 and y+1 <=17 then checkgrid[x-i][y+1] = VALID end
	end
	for i=1,block.s do
		if y+i <=17 then checkgrid[x][y+i] = VALID end
	end
end

function playSound(sound,count)
	if(sound ~= nil) then
		sound:play()
		if count ~= nil then
			delay(count)
		end
	end
end

function delay(count)
	for i=1,count do
		screen.waitVblankStart()
	end
end

function doDrop(timer)
	return timer > dropdelay / ((level/5)+1)
end

function countCircuit()
	circuitcount = circuitcount +1
end

function addScore(points)
	score = score + points
	if math.floor(score / levelup) > level then
		level = math.floor(score / levelup)
		playSound(snd_levelup)
	end
	if points > 1 then
		penaltyCount = penaltyCount + (points*20)
		if penaltyCount > penalty then
			penaltyCount = penalty
		end
	end
end


function checkForCircuit(x,y,blockset)

local rootnode = false

if blockset == nil then
	blockset = {}
	rootnode = true
end

	local count = 0;
	local check;
	
	table.insert(blockset,{x,y})

	if grid[x][y] ~= nil and grid[x][y].tile ~= 1 then

		-- mark this position in checkgrid
		checkgrid[x][y] = PENDING

		local b = getBlock(grid[x][y])
		
		-- check all connected blocks
		if b.n then -- CONNECTS NORTH
			if y<=1                              -- open to top border
			or grid[x][y-1]==nil                 -- open to empty space
			or not getBlock(grid[x][y-1]).s      -- doesn't connect to block above
			or checkgrid[x][y-1]==INVALID
			then
				checkgrid[x][y] = INVALID
				return false
			end
			if checkgrid[x][y-1]==nil then        -- check connecting block (if not being checked)
				check = checkForCircuit(x,y-1,blockset)
				if check==false then
					checkgrid[x][y] = INVALID
					if rootnode then invalidate(blockset) end
					return false
				else count = count + check end
			end
		end
		
		if b.e then -- CONNECTS EAST
			if x>=10                             -- open to right border
			or grid[x+1][y]==nil                 -- open to empty space
			or not getBlock(grid[x+1][y]).w      -- doesn't connect to right block
			or checkgrid[x+1][y]==INVALID
			then 
				checkgrid[x][y] = INVALID
				return false
			end
			
			if checkgrid[x+1][y]==nil then        -- check connecting block (if not being checked)
				check = checkForCircuit(x+1,y,blockset)
				if check==false then
					checkgrid[x][y] = INVALID
					if rootnode then invalidate(blockset) end
					return false
				else count = count + check end
			end
		end

		if b.s then -- CONNECTS SOUTH
			if y>=17                             -- open to bottom border
			or grid[x][y+1]==nil                 -- open to empty space
			or not getBlock(grid[x][y+1]).n      -- doesn't connect to block below
			or checkgrid[x][y+1]==INVALID
			then
				checkgrid[x][y] = INVALID
				return false
			end
			
			if checkgrid[x][y+1]==nil then        -- check connecting block (if not being checked)
				check = checkForCircuit(x,y+1,blockset)
				if check==false then
					checkgrid[x][y] = INVALID
					if rootnode then invalidate(blockset) end
					return false
				else count = count + check end
			end
		end

		if b.w then -- CONNECTS WEST
			if x<=1                              -- open to left border
			or grid[x-1][y]==nil                 -- open to empty space
			or not getBlock(grid[x-1][y]).e      -- doesn't connect to left block
			or checkgrid[x-1][y]==INVALID
			then
				checkgrid[x][y] = INVALID
				return false
			end
			
			if checkgrid[x-1][y]==nil then        -- check connecting block (if not being checked)
				check = checkForCircuit(x-1,y,blockset)
				if check==false then
					checkgrid[x][y] = INVALID
					if rootnode then invalidate(blockset) end
					return false
				else count = count + check end
			end
		end
		-- if haven't returned false by now, all "child" blocks are valid
		checkgrid[x][y] = VALID
		return count+1
	else
		-- there is no block here to check
		checkgrid[x][y] = INVALID
		return false
	end
	
end

function invalidate(blockset)
	for key,value in blockset do
		checkgrid[value[1]][value[2]] = INVALID
	end
end

-- take a number and display its digits on the screen at x,y
-- (x,y is screen coordinate, not grid reference)
function drawDigits(value,x,y)
	-- can only display numbers
	if type(value) == "number" then
		-- but treat it as a string in order to grab the digits
		for i=1,string.len(value) do
			digit = string.sub(value,i,i)
			screen:blit(x,(i-1)*10+y,img_text,48,digit*10,16,10)
		end
	end
end

-- draw active game elements: next block, current block, score etc
-- (grid is done separately for loop efficiency)
function drawGameState()

	--draw nextblock
	drawBlock(5,0,nextblock,1)
	
	--draw currentblock
	drawBlock(pos.x,pos.y,currentblock,2)
	
	--draw score: scorebox is 9 digits wide, starting at 457,9
	-- score is right-aligned
	drawDigits(score,457,(9 - string.len(score)) * 10 + 9)

	-- draw battery state
	if string.len(score) < 9 then
		-- can't do this in the test player
		--local battery = 100
		local battery = System.powerGetBatteryLifePercent()
		
		
		if     battery > 90 then screen:blit(457,9,img_text,0,0,16,10)
		elseif battery > 75 then screen:blit(457,9,img_text,0,10,16,10)
		elseif battery > 50 then screen:blit(457,9,img_text,0,20,16,10)
		elseif battery > 25 then screen:blit(457,9,img_text,0,30,16,10)
		else                     screen:blit(457,9,img_text,0,40,16,10)
		end
	end
	
	--draw info (level, bonus etc): box is 9 digits wide, starting at 457,173
	if chain > 1 then
		screen:blit(457,173,img_text,32,50,16,50) -- CHAIN
		screen:blit(457,233,img_text,16,0,16,10)   -- X
		drawDigits(chain,457,243)                 -- value 
	elseif state == "gameover" then
		screen:blit(457,173,img_text,16,10,16,90)  -- GAME OVER
	else	
		-- LEVEL <n>
--		screen:blit(457,173,img_text,32,0,16,50)  -- LEVEL
--		drawDigits(level,457,233)                 -- value

		-- [penalty]  <level n>
		if penalty > 0 then
			local penaltyBar = math.ceil(penaltyCount / penalty * 54)
			screen:fillRect(458,174,14, penaltyBar,RED)
		end
		
		drawDigits(level,457,(9 - string.len(level)) * 10 + 173)
	end

end

function setState(s)
	-- initial state is "title"
	if s == "help" then
		-- show the help screens
		playSound(snd_select,20)
		loadHelp()
	elseif s == "ready" then
		-- prepare the game and wait for player
		playSound(snd_select,20)
		gameInit()
		
	elseif s == "play" then
		-- active game
		playSound(snd_select,20)
		
	elseif s == "pause" then
		-- paused game
		playSound(snd_select,20)
		
	elseif s == "circuit" then
		animframe = 1;
		-- circuit has been completed, animate & remove

	elseif s == "fall" then
		-- circuit removed, loose blocks fall
		
	elseif s == "check" then
		-- blocks have fallen, check for new circuits
	
	elseif s == "penalty" then
		-- penalty timer elapsed; add penalty row

	elseif s == "gameover" then
		-- game has ended
		playSound(snd_gameover)

	elseif s == "quit" then
		-- exit game
		playSound(snd_select,30)
	
	end
	state = s
end

function initGrid()
	local g = {}
	for x=1,10 do
		g[x] = {}
		for y=1,17 do
			g[x][y] = nil
		end
	end
	return g
end



-- INITIALISATION

state = "title"

img_logo = Image.load("graphics/pineapple.png")
img_title = Image.load("graphics/title.png")

snd_move = Sound.load("sound/click.wav")
snd_select = Sound.load("sound/activate.wav")
snd_close = Sound.load("sound/zap.wav")

-- INTRO

screen:fillRect(0,0,480,272,BLACK)
screen.flip()
delay(30)

playSound(snd_close)
screen:fillRect(0,0,480,272,BLACK)
screen:blit( 60, 50, img_logo,0,0,360,150,true)
screen.flip()
delay(5)

screen:fillRect(0,0,480,272,BLACK)
screen:blit( 60, 50, img_logo,0,150,360,150,true)
screen.flip()
delay(5)

screen:fillRect(0,0,480,272,BLACK)
screen:blit( 60, 50, img_logo,0,300,360,150,true)
screen.flip()
delay(90)

screen:fillRect(0,0,480,272,BLACK)
screen.flip()
delay(30)

-- MAIN LOOP

lastkeyin = Controls.read()

while true do
	dopaint = true
	local keyin = Controls.read()
	
	-- always quit on SELECT
	if keyin:select() then
		setState("quit");
	end
	
	-- process game based on current state
	if state == "title" then
	
		if keyin ~= lastkeyin then
			if keyin:cross() then
				setState("help")
			end
			if keyin:start() then
				setState("ready");
			end
		end
	
		screen:blit(0,0,img_title)


	elseif state == "help" then
	
		if keyin ~= lastkeyin then
			if keyin:left() then
				currenthelp = currenthelp - 1;
				if currenthelp < 1 then
					currenthelp = 1
				else
					playSound(snd_move)
				end
			end
			if keyin:right() then
				currenthelp = currenthelp + 1;
				if currenthelp > 10 then
					currenthelp = 10
				else
					playSound(snd_move)
				end
			end
			if keyin:start() then
				setState("ready");
			end
		end

		screen:blit(0,0,img_help[currenthelp])
		
		
	elseif state == "ready" then
	
		if keyin ~= lastkeyin then
			if keyin:start() then
				setState("play");
			end
		end
	
		screen:blit(0,0,img_ready)
		
		
	elseif state == "play" then
		
		penaltyCount = penaltyCount - 1;
		
		if penaltyCount < 300
		and penaltyCount ~= 0
		and math.mod(penaltyCount,60)==0 then
			playSound(snd_alarm)
		end
		
		if penaltyCount < 0 then setState("penalty") end

		droptimer = droptimer + 1;
		
		--drop currentblock 1 square if appropriate 
		if (keyin:left()) or doDrop(droptimer) then
			if pos.y < 17 and grid[pos.x][pos.y+1] == nil then
				pos.y = pos.y + 1
			else
				if(pos.y == 0) then
					
					setState("gameover")
				else
					playSound(snd_drop)
					
					grid[pos.x][pos.y] = currentblock;
					if isEraser(currentblock) then
						if pos.y==17 then addScore(ebonus) end
						eraseBlocks(pos.x,pos.y,currentblock)
						setState("circuit")
						nextBlock()
					else
						addScore(1)

						checkgrid = initGrid()
						local check = checkForCircuit(pos.x,pos.y)
						if check ~= false then
							countCircuit()
							addScore(check*multiplier)
							setState("circuit")
							if     check >= emin[3] then nextEraser(3)
							elseif check >= emin[2] then nextEraser(2)
							elseif check >= emin[1] then nextEraser(1)
							else nextBlock()
							end
						else
							nextBlock()
						end
					
						--delay for a moment in case player is still holding down "drop"
						delay(10)
					end
				end
			end
			droptimer = 0;
		end
		
		-- other controls
		if keyin ~= lastkeyin then
			if keyin:left() then
				-- action handled above; sound should only play once
				playSound(snd_move)
				
			elseif keyin:up() then
				playSound(snd_move)
				if pos.y > 0 and pos.x > 1 and grid[pos.x-1][pos.y] == nil then
					pos.x = pos.x -1
				end
				
			elseif keyin:down() then
				playSound(snd_move)
				if pos.y > 0 and pos.x < 10 and grid[pos.x+1][pos.y] == nil then
					pos.x = pos.x +1
				end
			
			elseif keyin:right() then
				playSound(snd_move)
				rotateBlock(currentblock)
			
			elseif keyin:start() then
				setState("pause")
			end
		end
			
		
		--draw background
		screen:blit(0,0,img_bg)
		
		--draw grid
		for x=1,10 do
			for y=1,17 do
				if grid[x][y] ~= nil then
					drawBlock(x,y,grid[x][y],2)
				end
			end
		end

		drawGameState()
		
	elseif state == "pause" then
	
		if keyin ~= lastkeyin then
			if keyin:start() then
				setState("play");
			end
		end
	
		screen:blit(0,0,img_ready)
		
	elseif state == "circuit" then
		
		--draw background
		screen:blit(0,0,img_bg)
		
		--draw grid
		for x=1,10 do
			for y=1,17 do
				if grid[x][y] ~= nil then
					if checkgrid[x][y]==VALID then
						-- animate closed circuit
						if animframe <= table.getn(sequence) then
							drawBlock(x,y,grid[x][y],sequence[animframe])
						else
							-- remove blocks from closed circuit
							grid[x][y] = nil
						end
					else
						drawBlock(x,y,grid[x][y],2)
					end
				end
			end
		end
		
		drawGameState()

		if animframe > table.getn(sequence) then
			setState("fall")
		else
			playSound( animsound[animframe] )
			delay(2)
			animframe = animframe + 1
		end

	elseif state == "fall" then
	
		--draw background
		screen:blit(0,0,img_bg)
		
		local falling = false
		--draw grid; start from bottom to check for gaps
		for x=1,10 do
			for y=17,1,-1 do
				if grid[x][y] ~= nil then
					if y<17 and grid[x][y+1] == nil then
						falling = true
						grid[x][y+1] = grid[x][y]
						grid[x][y] = nil
						drawBlock(x,y,grid[x][y+1],2)
						
						if y>=16 or grid[x][y+2] ~= nil then
							-- block has hit something
						end
					else
						drawBlock(x,y,grid[x][y],2)
					end
				end
			end
		end
		
		drawGameState()
		
		delay(5)
		
		
		if not falling then
			-- check grid for circuits after fall
			checkgrid = initGrid()
			local circuitFound = false
			local chainscore = 0
			for x=1,10 do
				for y=17,1,-1 do
					if grid[x][y] ~= nil and checkgrid[x][y]==nil then
						local check = checkForCircuit(x,y)
						if check~=false then
							countCircuit()
							chainscore = chainscore + check
							circuitFound = true
						end
					end
				end
			end
			if circuitFound then
				chain = chain + 1
				addScore(chainscore*chain*multiplier)
				nextEraser(3) -- chains automatically grant level 3 eraser 
				setState("circuit")
			else
				if chain > 1 then
					for i=1,chain do
						playSound(snd_bonus,20)
					end
				end
				chain = 1
				state = "play"
			end
		end
		
		elseif state == "penalty" then
		
		--draw background
		screen:blit(0,0,img_bg)
		
		--draw grid
		
		local pasttop = false
		
		for x=1,10 do
			for y=1,17 do
				if y == 1 and grid[x][y] ~= nil then
					pasttop = true
				else
					-- move this block up one row
					grid[x][y-1] = grid[x][y]
					if grid[x][y] ~=nil then
						drawBlock(x,y-1,grid[x][y-1],2)
					end
				end
				if y == 17 then
					grid[x][y] = penaltyBlock()
					drawBlock(x,y,grid[x][y],2)
				end
			end
		end
		
		playSound(snd_drop)
		
		penaltyCount = penalty;

		if grid[pos.x][pos.y] ~=nil then
			if pos.y > 1 then
				pos.y = pos.y - 1
			else
				pasttop = true
			end
		end

		if pasttop then
			setState("gameover")
		else
			state = "play"
		end
		
		drawGameState()
		
		delay(5)

	
	elseif state == "gameover" then
	
		dopaint = false
	
		if keyin ~= lastkeyin then
			if keyin:start() then
				setState("ready");
			end
		end
		
	elseif state == "quit" then
		break;
		
	end

	lastkeyin = keyin
	
	-- show updated screen
	if dopaint then
		screen.flip()
	end
	screen.waitVblankStart()
end
