From e72226dbf8f61dacc2fe4d18297807faa27b207f Mon Sep 17 00:00:00 2001
From: piernov <piernov@piernov.org>
Date: Mon, 12 May 2014 13:00:01 +0200
Subject: Multiplayer mode + various improvement

---
 Gamestates/About.lua                |  30 ++++++
 Gamestates/Config.lua               |  64 ++++++++++++
 Gamestates/Game.lua                 |  65 ++++++++++++
 Gamestates/Init.lua                 |   4 -
 Gamestates/Menu.lua                 |  48 ++++++++-
 Gamestates/Multiplayer.lua          |  18 +++-
 Gamestates/Multiplayer/InGame.lua   | 201 ++++++++++++++++++++++++++++++++++++
 Gamestates/Multiplayer/Internet.lua |   0
 Gamestates/Multiplayer/Local.lua    | 165 +++++++++++++++++++++--------
 Gamestates/Multiplayer/Protocol.lua |  35 +++++++
 Gamestates/Solo.lua                 | 121 ++++++++++++----------
 11 files changed, 644 insertions(+), 107 deletions(-)
 create mode 100644 Gamestates/About.lua
 create mode 100644 Gamestates/Config.lua
 create mode 100644 Gamestates/Game.lua
 delete mode 100644 Gamestates/Init.lua
 create mode 100644 Gamestates/Multiplayer/InGame.lua
 delete mode 100644 Gamestates/Multiplayer/Internet.lua
 create mode 100644 Gamestates/Multiplayer/Protocol.lua

(limited to 'Gamestates')

diff --git a/Gamestates/About.lua b/Gamestates/About.lua
new file mode 100644
index 0000000..d65b245
--- /dev/null
+++ b/Gamestates/About.lua
@@ -0,0 +1,30 @@
+--[[
+	Gamestates/About.lua
+	About page
+	by piernov
+]]--
+local Gui = require "Quickie"
+local Utils = require "Utils"
+
+local About = {}
+
+function About:enter()
+	Previous = "Gamestates/Menu"
+end
+
+function About:update(dt)
+	Gui.group{grow = "down", pos = {Utils.percentCoordinates(10, 10)}, function()
+		Gui.Label{text = "BlueMind", size = {Utils.percentCoordinates(10, 10)}}
+		Gui.Label{text = "A Mastermind(tm)-like game", size = {Utils.percentCoordinates(10, 10)}}
+		Gui.Label{text = "by piernov and Roildan", size = {Utils.percentCoordinates(10, 10)}}
+		Gui.Label{text = "Music by Choucka", size = {Utils.percentCoordinates(10, 10)}}
+
+	end}
+end
+
+function About:draw()
+	Gui.core.draw()
+end
+
+
+return About
diff --git a/Gamestates/Config.lua b/Gamestates/Config.lua
new file mode 100644
index 0000000..394b115
--- /dev/null
+++ b/Gamestates/Config.lua
@@ -0,0 +1,64 @@
+--[[
+	Gamestates/Config.lua
+	User defined configuration
+	by piernov
+
+	Comment: currently only used for player_name, but maybe allowing other parameters like sound or display settings will be implemented.
+		A similar file could be used to implement an "High score" feature.
+]]--
+
+Config = {}
+Config.player_name = {}
+
+local Gui = require "Quickie"
+local Utils = require "Utils"
+
+function Config:loadUserConfig() -- Load user config file
+	if not love.filesystem.exists("userConfig") then
+		love.filesystem.write( "userConfig", "") -- Create the save file if it doesn't exists
+	else
+		for line in love.filesystem.lines("userConfig") do -- Read line by line
+			var, arg = line:match("(.*): (.*)") -- Split following the "<KEY>: <VALUE>" schema
+			if var == "PLAYERNAME" then
+				Config.player_name.text = arg -- Handle player_name
+			end
+		end
+	end
+	if not Config.player_name.text then
+		Config.player_name.text = "BlueMind" -- Set default player_name if nothing was found in the user config file
+	end
+end
+
+function Config:enter()
+	Previous = "Gamestates/Menu"
+end
+
+function Config:update(dt)
+	Gui.group.push{grow = "down", pos = {Utils.percentCoordinates(20, 20)}}
+		Gui.Label{text = "Player name", size = {Utils.percentCoordinates(10, 10)}}
+		Gui.Input{info = Config.player_name, size = {Utils.percentCoordinates(50, 10)}}
+		if Gui.Button{text = "Save", size = {Utils.percentCoordinates(60, 10)}} then
+			love.filesystem.write( "userConfig", "PLAYERNAME: " .. Config.player_name.text) -- Write the new name to the config file
+		end
+	Gui.group.pop{}
+end
+
+-- Quickie's functions
+function Config:draw()
+	Gui.core.draw()
+end
+
+function Config:keypressed(key, code)
+    Gui.keyboard.pressed(key)
+end
+
+function love.textinput(str)
+    Gui.keyboard.textinput(str)
+end
+-- End
+
+function Config:leave()
+	love.keyboard.setTextInput(false)
+end
+
+return Config
diff --git a/Gamestates/Game.lua b/Gamestates/Game.lua
new file mode 100644
index 0000000..3bd870a
--- /dev/null
+++ b/Gamestates/Game.lua
@@ -0,0 +1,65 @@
+--[[
+	Gamestates/Game.lua
+	Shared game functions between Solo and Multiplayer mode
+	by piernov
+
+	Comment: this should even get more functions shared like mousepressed.
+]]--
+
+local Game = {}
+
+function Game.checkKeys(received, answer) -- Check the submitter answer against the real one
+	local hints = {}
+
+	for id, num in ipairs(received) do
+		if tonumber(num) == answer[id] then -- Right
+			table.insert(hints, 1)
+		else -- Misplaced or Wrong
+			for k,v in ipairs(answer) do
+				if tonumber(num) == v then -- Misplaced
+					table.insert(hints, 0)
+				end
+			end
+		end
+	end
+
+	return hints
+end
+
+function Game.addHints(hints, lineNumber) -- Generate hints rectangles from the checkKeys()-returned table
+	local polygons = {}
+	for i=1,#hints do
+		if tonumber(hints:sub(i, i)) == 1 then -- Show "misplaced" hint
+			table.insert(polygons, { Type = "rectangle", LineWidth = 0.0025, DrawMode = "fill",
+				Position = { x = 0.795+((#polygons-1)%2)*0.0325, y = 0.12+math.floor(#polygons/2)*0.0325+(lineNumber-1)*0.075},
+				Dimension = {width = 0.0225, height = 0.025}, Colors = {255, 0, 171, 255} })
+		else -- Show "well placed" hint
+			table.insert(polygons, { Type = "rectangle", LineWidth = 0.0025, DrawMode = "fill",
+				Position = { x = 0.795+((#polygons-1)%2)*0.0325, y = 0.12+math.floor(#polygons/2)*0.0325+(lineNumber-1)*0.075},
+				Dimension = {width = 0.0225, height = 0.025}, Colors = {171, 171, 171, 255} })
+		end
+	end
+	return polygons
+end
+
+
+function Game.genAnswer() -- Generate a random answer
+	local allowedNumbers = { 1, 2, 3, 4, 5, 6 } -- Select between those numbers, only 6 possibility
+	local tempAnswer = {}
+
+	while #tempAnswer < 4 do -- Pick 4 numbers
+		local n = love.math.random(#allowedNumbers) -- Randomly in allowedNumbers
+		table.insert(tempAnswer, allowedNumbers[n]) -- Add the number to the answer
+		table.remove(allowedNumbers, n) -- And delete it from allowedNumbers to prevent picking the same number a second time
+	end
+
+	return tempAnswer
+end
+
+function Game.init()
+	if not love.getVersion then -- Check if LÖVE is older than 0.9.1, getVersion() introduced in version 0.9.1
+		love.math.random() -- Throw first value since it's not random in LÖVE 0.9.0
+	end
+end
+
+return Game
diff --git a/Gamestates/Init.lua b/Gamestates/Init.lua
deleted file mode 100644
index 5f42661..0000000
--- a/Gamestates/Init.lua
+++ /dev/null
@@ -1,4 +0,0 @@
-return {
-	Solo = require("Gamestates/Solo"),
-	Multiplayer = require("Gamestates/Multiplayer")
-}
diff --git a/Gamestates/Menu.lua b/Gamestates/Menu.lua
index cf612bd..37e69a4 100644
--- a/Gamestates/Menu.lua
+++ b/Gamestates/Menu.lua
@@ -1,13 +1,57 @@
+--[[
+	Gamestates/Menu.lua
+	Main menu
+	by piernov
+
+	Comment: might not be a good idea to immediately load both Solo and Multiplayer mode.
+]]--
+
 local Menu = {}
 local Gui = require "Quickie"
 local Utils = require "Utils"
 local GUI = { Menu = require("GUI/Menu")}
-local Gamestates = require "Gamestates/Init"
+local Gamestates = {
+	Solo = require("Gamestates/Solo"),
+	Multiplayer = require("Gamestates/Multiplayer"),
+	About = require("Gamestates/About")
+}
+Gamestates.Options = Config
+
+
+function Menu:enter()
+	Previous = nil -- Menu's root
+
+-- Redefine Quickie's colors
+	Gui.core.style.color.normal.fg[1] = 0
+	Gui.core.style.color.normal.fg[2] = 0
+	Gui.core.style.color.normal.fg[3] = 255
+
+	Gui.core.style.color.normal.bg[1] = 0
+	Gui.core.style.color.normal.bg[2] = 0
+	Gui.core.style.color.normal.bg[3] = 0
+
+	Gui.core.style.color.hot.fg[1] = 0
+	Gui.core.style.color.hot.fg[2] = 0
+	Gui.core.style.color.hot.fg[3] = 255
+
+	Gui.core.style.color.hot.bg[1] = 48
+	Gui.core.style.color.hot.bg[2] = 48
+	Gui.core.style.color.hot.bg[3] = 48
+
+	Gui.core.style.color.active.fg[1] = 0
+	Gui.core.style.color.active.fg[2] = 0
+	Gui.core.style.color.active.fg[3] = 255
+
+	Gui.core.style.color.active.bg[1] = 24
+	Gui.core.style.color.active.bg[2] = 24
+	Gui.core.style.color.active.bg[3] = 24
+-- End
+end
 
 function Menu:update(dt)
 	Gui.group{grow = "down", pos = {Utils.percentCoordinates(10, 10)}, function()
 		for _, name in ipairs(GUI.Menu.Buttons) do
-			if Gui.Button{text = name, size = {Utils.percentCoordinates(80, 20)}} then
+			if Gui.Button{text = name, size = {Utils.percentCoordinates(80, 20)}} then -- Display main menu buttons and switch gamestate if clicked
 				Gamestate.switch(Gamestates[name])
 			end
 		end
diff --git a/Gamestates/Multiplayer.lua b/Gamestates/Multiplayer.lua
index 879a100..0347238 100644
--- a/Gamestates/Multiplayer.lua
+++ b/Gamestates/Multiplayer.lua
@@ -1,18 +1,26 @@
+--[[
+	Gamestates/Multiplayer.lua
+	Multiplayer menu
+	by piernov
+
+	Comment: created for an hypothetical Internet mode.
+]]--
+
 local Multiplayer = {
-	Local = require "Gamestates/Multiplayer/Local",
-	Internet = require "Gamestates/Multiplayer/Internet"}
+	Local = require "Gamestates/Multiplayer/Local"}
 
 local Gui = require "Quickie"
 local Utils = require "Utils"
 
+function Multiplayer:enter()
+	Previous = "Gamestates/Menu"
+end
+
 function Multiplayer:update(dt)
 	Gui.group{grow = "down", pos = {Utils.percentCoordinates(10, 10)}, function()
 		if Gui.Button{text = "Local", size = {Utils.percentCoordinates(80, 20)}} then
 			Gamestate.switch(Multiplayer.Local)
 		end
-		if Gui.Button{text = "Internet", size = {Utils.percentCoordinates(80, 20)}} then
-			Gamestate.switch(Multiplayer.Internet)
-		end
 	end}
 end
 
diff --git a/Gamestates/Multiplayer/InGame.lua b/Gamestates/Multiplayer/InGame.lua
new file mode 100644
index 0000000..f94be12
--- /dev/null
+++ b/Gamestates/Multiplayer/InGame.lua
@@ -0,0 +1,201 @@
+--[[
+	Gamestates/Multiplayer/InGame.lua
+	Multiplayer mode
+	by piernov
+]]--
+local GUI = { InGame = require("GUI/InGame")}
+local Game = require("Gamestates/Game")
+local Protocol = require("Gamestates/Multiplayer/Protocol")
+
+local InGame = {}
+
+function InGame:enter(Local) -- Initialize or reset variables when entering game
+	Previous = "Gamestates/Multiplayer/Local"
+	InGame.Keypressed = {{}} -- 2D array for storage of button pressed
+	InGame.Hints = {} -- Hints displayed for local player
+	InGame.opponentHints = {} -- Hints displayed for remote player
+	InGame.state = "playing" -- Default to "playing" but it doesn't mean anything
+	InGame.localLife = {} 
+	InGame.remoteLife = {}
+
+	InGame.host = Local.host
+	InGame.client = Local.client
+
+	InGame.opponentAnswer = Game.genAnswer() -- Generate answer for the opponent
+
+	for o = 0, 9	do
+	table.insert(InGame.remoteLife, { Type = "line", LineWidth = 0.015, Position = { x1 = 0.135, y1 = 0.61+0.02*o, x2 = 0.185, y2 = 0.61+0.02*o}, Colors = {171, 0, 0, 255} }) -- Display life bar content
+	end
+
+	for o = 0, 9	do
+	table.insert(InGame.localLife, { Type = "line", LineWidth = 0.022, Position = { x1 = 0.535+0.0275*o, y1 = 0.66, x2 = 0.535+0.0275*o, y2 = 0.71}, Colors = {171, 0, 0, 255} }) -- Display life bar content
+	end
+
+	InGame:resize() -- Call the function filling the canvas
+
+	InGame.Musiclowlife = love.audio.newSource("Resources/BlueMind midlife.ogg")
+	InGame.Music = love.audio.newSource("Resources/BlueMind.ogg")
+	InGame.Music:setLooping(true)
+	InGame.Music:play()
+end
+
+function InGame:resize() -- Called in :enter() and when the window is resized
+	InGame.Display = love.graphics.newCanvas(Screen.width, Screen.height) -- Create the canvas containing base interface to avoid drawing it entirely each frame
+	love.graphics.setCanvas(InGame.Display)
+		love.graphics.setFont(Fonts[3]) -- Use font with size 3% by default
+		InGame.Display:clear()
+		for id, polygon in ipairs(GUI.InGame.Polygons) do
+			GUI.InGame.drawPolygon(polygon)
+		end
+	love.graphics.setCanvas()
+end
+
+function InGame:init(host, client)
+	Game.init()
+	GUI.InGame.loadInterface()
+	GUI.InGame.loadMultiplayerInterface()
+end
+
+function InGame:draw()
+	love.graphics.setColor(255, 255, 255, 255)
+	love.graphics.setBlendMode('premultiplied')
+	love.graphics.draw(InGame.Display)
+	love.graphics.setBlendMode('alpha')
+
+	for num, line in ipairs(InGame.Hints) do
+		for k, poly in ipairs(line) do
+			GUI.InGame.drawPolygon(poly)
+		end
+	end
+
+	for line, keys in ipairs(InGame.Keypressed) do
+		for id, num in ipairs(keys) do
+			GUI.InGame.drawPolygon({ Type = "rectangle", DrawMode = "fill", Position = { x = 0.50+(id-1)*0.075, y = 0.15+(line-1)*0.075-0.025}, Dimension = {width = 0.04, height = 0.04}, Colors = GUI.InGame.Colors[num] })
+		end
+	end
+
+	for num, poly in ipairs(InGame.localLife) do
+		GUI.InGame.drawPolygon(poly)
+	end
+
+	for num, poly in ipairs(InGame.remoteLife) do
+		GUI.InGame.drawPolygon(poly)
+	end
+
+	for id, num in ipairs(InGame.opponentAnswer) do
+		GUI.InGame.drawPolygon({ Type = "rectangle", DrawMode = "fill", Position = { x = 0.08+(id-1)*0.075+0.005, y = 0.12+0.005}, Dimension = {width = 0.035, height = 0.04}, Colors = GUI.InGame.Colors[num] }) -- Top left boxes content displaying opponent answer
+	end
+
+-- Display popup for win/loose and found answer
+	if InGame.state == "found" then
+		GUI.InGame.displayPopup("Found !")
+	elseif InGame.state == "not-found" then
+		GUI.InGame.displayPopup("Not found...")
+	elseif InGame.state == "win" then
+		GUI.InGame.displayPopup("You win !")
+	elseif InGame.state == "loose" then
+		GUI.InGame.displayPopup("You loose...")
+	end
+-- End
+
+	GUI.InGame.drawPolygon({ Type = "print", Text = InGame.client.name, Position = { x = 0.15, y = 0.06}, Colors = {0, 0, 171, 255} }) -- Display remote player's name
+	GUI.InGame.drawPolygon({ Type = "print", Text = Config.player_name.text, Position = { x = 0.60, y = 0.06}, Colors = {0, 0, 171, 255} }) -- Display local player's name
+end
+
+function InGame:update(dt)
+	local event = InGame.host:service()
+
+	while event ~= nil do -- Handle network events
+		Protocol.parseGlobalEvents(event)
+
+		if event.type == "receive" then
+			command, args = event.data:match("(.*): (.*)") -- Split received message : "<COMMAND>: <ARGS>"
+			if command == "ANSWER" then
+				local hints = table.concat(Game.checkKeys({args:match("(%d)(%d)(%d)(%d)")}, InGame.opponentAnswer)) -- Process received answers
+				if hints == "1111" then -- Only well-placed hints = answer found
+					event.peer:send("FOUND: TRUE", 0, "reliable")
+					table.remove(InGame.localLife, 1) -- Loss of 1 life point
+					if #InGame.localLife < 1 then -- Empty life
+						InGame.state = "loose"
+					else
+						if #InGame.localLife == 2 then
+							InGame.Music:stop()
+							InGame.Musiclowlife:setLooping(true)
+							InGame.Musiclowlife:play()
+						end
+						InGame.opponentAnswer = Game.genAnswer() -- Generate a new answer
+						InGame.opponentHints = {}
+					end
+				else
+					event.peer:send("HINTS: " .. hints, 0, "reliable")
+				end
+			elseif command == "FOUND" and args == "TRUE" then
+				table.remove(InGame.remoteLife, 1) -- Opponent loose 1 life point
+				if #InGame.remoteLife < 1 then -- Opponent's life is empty, we won
+					InGame.state = "win"
+				else
+					InGame.Keypressed = {{}}
+					InGame.Hints = {}
+					InGame.state = "found"
+				end
+			elseif command == "FOUND" and args == "FALSE" then
+				table.remove(InGame.remoteLife, 1) -- Opponent loose 1 life point
+				InGame.opponentAnswer = Game.genAnswer() -- Generate a new answer
+				InGame.opponentHints = {}
+			elseif command == "HINTS" then
+				table.insert(InGame.Hints, Game.addHints(args, #InGame.Keypressed))
+				InGame.Keypressed[#InGame.Keypressed+1] = {}
+				InGame.state = "playing"
+				if #InGame.Keypressed > 7 then
+					table.remove(InGame.localLife, 1) -- Loss of 1 life point
+					InGame.Keypressed = {{}}
+					InGame.Hints = {}
+					InGame.state = "not-found"
+					event.peer:send("FOUND: FALSE", 0, "reliable")
+				end
+			end
+		end
+		event = InGame.host:service() -- Process next event
+	end
+end
+
+function InGame:mousepressed(x, y, b) -- Handle mouse
+	if InGame.state == "found" or InGame.state == "not-found" then -- Click anywhere to hide the "found" popup message
+		InGame.state = "playing"
+	end
+
+	x = x/Screen.width -- relative coordinates
+	y = y/Screen.height
+	if 0.875 < y and y < 0.915 and InGame.state ~= "validating" then -- lower part of the screen, and don't do anything if we're waiting for hints from peer
+		if x > 0.45 then
+			for i = 0,5 do
+				if 0.50+i*0.075 < x and x < 0.50+i*0.075+0.04 then
+					if #InGame.Keypressed > 7 or #InGame.Keypressed[#InGame.Keypressed] >= 4  then -- Line already full
+						return
+					else
+						for k,v in pairs(InGame.Keypressed[#InGame.Keypressed]) do
+							if v == i+1 then
+								return -- Disallow multiple identical colors in same line
+							end
+						end
+
+						table.insert(InGame.Keypressed[#InGame.Keypressed], i+1) -- Click detected on a color-button
+					end
+				end
+			end
+		elseif 0.25 < x and x < 0.375 and #InGame.Keypressed <= 7 and #InGame.Keypressed[#InGame.Keypressed] == 4 then -- Clik on Ok button
+			local peer = InGame.host:get_peer(InGame.client.index)
+			InGame.state = "validating"
+			peer:send("ANSWER: " .. table.concat(InGame.Keypressed[#InGame.Keypressed]), 0, "reliable")
+		elseif #InGame.Keypressed <= 7 and 0.075 < x and x < 0.200 and #InGame.Keypressed[#InGame.Keypressed] > 0 then -- Click on reset button
+			InGame.Keypressed[#InGame.Keypressed] = {}
+		end
+	end
+end
+
+function InGame:leave()
+	InGame.Music:stop()
+	InGame.Musiclowlife:stop()
+end
+
+return InGame
diff --git a/Gamestates/Multiplayer/Internet.lua b/Gamestates/Multiplayer/Internet.lua
deleted file mode 100644
index e69de29..0000000
diff --git a/Gamestates/Multiplayer/Local.lua b/Gamestates/Multiplayer/Local.lua
index aaa13f6..7158866 100644
--- a/Gamestates/Multiplayer/Local.lua
+++ b/Gamestates/Multiplayer/Local.lua
@@ -1,3 +1,11 @@
+--[[
+	Gamestates/Multiplayer/Local.lua
+	Local connect menu
+	by piernov
+
+	Comment: we really miss a Bonjour/Avahi Lua binding, only Android NSD is (barely) supported…
+]]--
+
 local Local = {}
 
 require "enet"
@@ -5,16 +13,20 @@ require "enet"
 local Gui = require "Quickie"
 local Utils = require "Utils"
 
+local Protocol = require "Gamestates/Multiplayer/Protocol"
+
 local connect, connected = false, false
 Local.server_host = { text = "" }
-Local.myservice = { name = "Mastermind", ip = "*", port = "5678" }
+Local.myservice = { name = "BlueMind", ip = "*", port = 5678 }
+Local.client = {}
 
-if love.system.getOS() == "Android" then
+-- Android NSD events callback
+if love.system.getOS() == "Android" then -- Won't work on other OS…
 	Local.services = {}
 
-
 	function love.handlers.serviceregistered(a,b,c,d)
 		SDL.log("ServiceRegistered: " .. a)
+		Local.myservice.name = a
 	end
 
 	function love.handlers.discoverystarted(a,b,c,d)
@@ -37,67 +49,128 @@ if love.system.getOS() == "Android" then
 		Local.services[a] = nil
 	end
 end
+-- End
+
+function Local:enter()
+	Previous = "Gamestates/Multiplayer"
+	connect, connected = false, false
+end
 
 function Local:init()
-	Local.host = enet.host_create(Local.myservice.ip .. ":" .. Local.myservice.port)
+	Local.host = enet.host_create(Local.myservice.ip .. ":" .. Local.myservice.port) -- Open network socket
+	local i = Local.myservice.port
+	while not Local.host do -- unable to create socket: most likely port is already used
+		if i > Local.myservice.port+20 then
+			print("Error while creating socket")
+			return 1
+		end
+		Local.myservice.port = Local.myservice.port+1 -- try next port
+		Local.host = enet.host_create(Local.myservice.ip .. ":" .. Local.myservice.port)
+	end
+
+	Local.myservice.name = Config.player_name.text -- Get player_name from user config
 
-	if love.system.getOS() == "Android" then
-		love.android.registerService(Local.myservice.name, "_http._tcp.", Local.myservice.port)
-	    love.android.discoverServices("_http._tcp.")
+	if love.system.getOS() == "Android" then -- Start Android NSD discovery and publish
+		love.android.registerService(Local.myservice.name, "_bluemind._tcp.", Local.myservice.port)
+	    love.android.discoverServices("_bluemind._tcp.")
 	end
 end
 
 function Local:update(dt)
 
-	if not connect then
-		Gui.group.push{grow = "right"}
+	Gui.group.push{grow = "right"}
 
-		Gui.group.push{grow = "down", pos = {Utils.percentCoordinates(10, 0)}}
-			if love.system.getOS() == "Android" then
-				for name, host in pairs(Local.services) do
-					if Gui.Button{text = name .. " " .. host.ip .. ":" .. host.port, size = {Utils.percentCoordinates(60, 10)}} then
-						Local.server_host.text = host.ip.. ":" .. host.port
-						connect = true
-						return
+	Gui.group.push{grow = "down", pos = {Utils.percentCoordinates(10, 0)}}
+		if love.system.getOS() == "Android" then -- List discovered services
+			for name, host in pairs(Local.services) do
+				if Gui.Button{text = name .. " " .. host.ip .. ":" .. host.port, size = {Utils.percentCoordinates(60, 10)}} then
+					Local.server_host.text = host.ip.. ":" .. host.port
+					if connected then
+						Local.host:disconnect()
+						connected = false
 					end
+					connect = true
+					return
 				end
 			end
+		end
+
+-- Manually entering peer's address
+		Gui.Label{text = "Host", size = {Utils.percentCoordinates(10, 10)}}
+		Gui.Input{info = Local.server_host, size = {Utils.percentCoordinates(50, 10)}}
+		if Gui.Button{text = "Connect", size = {Utils.percentCoordinates(60, 10)}} then
+			connect = true
+		end
+-- End
+
+-- Handle connection and display a proposition message
+		if Local.client.name and not connect then
+			Gui.Label{text = args .. " connected to you. Play a game ?"}
+
+			Gui.group.push{grow = "right"}
+				if Gui.Button{text = "Yes", size = {Utils.percentCoordinates(10, 10)}} then
+					Protocol.acceptGame(Local.host, Local.client.index, true)
+					Gamestate.switch(require("Gamestates/Multiplayer/InGame"))
+				end
+				if Gui.Button{text = "No", size = {Utils.percentCoordinates(10, 10)}} then
+					Protocol.acceptGame(Local.host, Local.client.index, false)
+					Local.client = {}
+				end
+			Gui.group.pop{}
+		end
+-- End
 
-			Gui.Label{text = "Host", size = {Utils.percentCoordinates(10, 10)}}
-			Gui.Input{info = Local.server_host, size = {Utils.percentCoordinates(50, 10)}}
-			if Gui.Button{text = "Connect", size = {Utils.percentCoordinates(60, 10)}} then
-				connect = true
-			end
-		Gui.group.pop{}
 
-		Gui.group.push{grow = "down", pos = {Utils.percentCoordinates(10, 0)}}
-			Gui.Label{text = "My infos"}
-			Gui.Label{text = "Name: " .. Local.myservice.name}
-			Gui.Label{text = "Host: " .. Local.myservice.ip .. ":" .. Local.myservice.port}
-		Gui.group.pop{}
+	Gui.group.pop{}
 
-		Gui.group.pop{}
-	elseif not connected then
+-- Show local player name and host
+	love.graphics.setFont(Fonts[3])
+	Gui.group.push{grow = "down", pos = {Utils.percentCoordinates(10, 0)}}
+		Gui.Label{text = "My infos"}
+		Gui.Label{text = "Name: " .. Local.myservice.name}
+		Gui.Label{text = "Host: " .. Local.myservice.ip .. ":" .. Local.myservice.port}
+	Gui.group.pop{}
+	love.graphics.setFont(Fonts[4])
+-- End
+
+	Gui.group.pop{}
+
+	if connect and not connected then
 		Local.host:connect(Local.server_host.text)
 		connected = true
-	else
-		local event = Local.host:service()
-
-		while event ~= nil do
-			if event.type == "receive" then
-				print("Receive ", event.data, event.peer)
-			elseif event.type == "connect" then
-				if event.peer:index() > 1 then
-					print("Already connected")
-				else
-					print("Local " .. tostring(event.peer) .. " " .. event.peer:state() .. " " .. tostring(event.peer:connect_id()))
+	end
+
+	local event = Local.host:service()
+
+	while event ~= nil do -- Process network events
+		Protocol.parseGlobalEvents(event)
+
+		if event.type == "receive" then
+			command, args = event.data:match("(.*): (.*)")
+			if command == "CONNECT" then
+				Local.client.name = args
+				event.peer:send("CONNECTED: ".. Local.myservice.name, 0, "reliable")
+			elseif command == "CONNECTED" then
+				Local.client.name = args
+			elseif command == "ACCEPT" then
+				if args == "YES" then
+					Gamestate.switch(require("Gamestates/Multiplayer/InGame"))
+				elseif args == "NO" then
+					event.peer:disconnect()
+					connect, connected = false, false
+					Local.client = {}
 				end
-			elseif event.type == "disconnect" then
-					print("Disconnect " .. tostring(event.peer))
 			end
-
-			event = Local.host:service()
+		elseif event.type == "connect" then
+			if event.peer:index() <= 1 then
+				Local.client.index = event.peer:index()
+			end
+			if connected then
+				event.peer:send("CONNECT: ".. Local.myservice.name, 0, "reliable")
+			end
 		end
+
+		event = Local.host:service()
 	end
 end
 
@@ -114,4 +187,8 @@ function love.textinput(str)
     Gui.keyboard.textinput(str)
 end
 
+function Local:leave()
+	love.keyboard.setTextInput(false)
+end
+
 return Local
diff --git a/Gamestates/Multiplayer/Protocol.lua b/Gamestates/Multiplayer/Protocol.lua
new file mode 100644
index 0000000..6f3b739
--- /dev/null
+++ b/Gamestates/Multiplayer/Protocol.lua
@@ -0,0 +1,35 @@
+--[[
+	Gamestates/Multiplayer/Protocol.lua
+	Some functions involved in talking between 2 clients
+	by piernov
+
+	Comment: maybe useless.
+]]--
+local Protocol = {}
+
+function Protocol.acceptGame(host, peer_id, accept) -- Send an answer to a connection from another player
+	local peer = host:get_peer(peer_id)
+	if accept then
+		peer:send("ACCEPT: YES", 0, "reliable")
+	else
+		peer:send("ACCEPT: NO", 0, "reliable")
+		peer:disconnect_later()
+	end
+end
+
+function Protocol.parseGlobalEvents(event) -- Shared function between local menu and game which disallow multiple connections and handle disconnect
+	if event.type == "connect" then
+		if event.peer:index() > 1 then
+			print("Already connected")
+			event.peer:disconnect(2)
+		end
+	elseif event.type == "disconnect" then
+		print("Disconnect " .. tostring(event.peer))
+		if event.data == 2 then
+			print("Peer is busy")
+			event.peer:disconnect()
+		end
+	end
+end
+
+return Protocol
diff --git a/Gamestates/Solo.lua b/Gamestates/Solo.lua
index 8d50ad6..7cd85da 100644
--- a/Gamestates/Solo.lua
+++ b/Gamestates/Solo.lua
@@ -1,62 +1,57 @@
+--[[
+	Gamestates/Solo.lua
+	Solo mode
+	by piernov
+]]--
+
 local GUI = { InGame = require("GUI/InGame")}
+local Game = require("Gamestates/Game")
 
 local Solo = {}
 
-Solo.Keypressed = {{}}
-Solo.Answer = {}
-Solo.Hints = {}
-
+function Solo:enter() -- Initialize or reset variables when entering game
+	Previous = "Gamestates/Menu"
+	Solo.Keypressed = {{}} -- 2D array for storage of button pressed
+	Solo.Hints = {} -- Hints displayed
+	Solo.state = "playing" -- Default to "playing" but it doesn't mean anything
 
-function Solo:checkKeys()
-	local found = true
-	Solo.Hints[#Solo.Hints+1] = {}
+	Solo.Answer = Game.genAnswer() -- Generate answer
 
-	for id, num in ipairs(Solo.Keypressed[#Solo.Keypressed]) do
-		if num == Solo.Answer[id] then -- Right
-			table.insert(Solo.Hints[#Solo.Hints], { Type = "rectangle", LineWidth = 0.0025, DrawMode = "fill",
-				Position = { x = 0.795+((#Solo.Hints[#Solo.Hints]-1)%2)*0.0325, y = 0.12+math.floor(#Solo.Hints[#Solo.Hints]/2)*0.0325+(#Solo.Keypressed-1)*0.075},
-				Dimension = {width = 0.0225, height = 0.025}, Colors = {0, 0, 171, 255} })
-		else -- Misplaced or Wrong
-			found = false
-
-			for k,v in ipairs(Solo.Answer) do
-				if num == v then -- Misplaced
-					table.insert(Solo.Hints[#Solo.Hints], { Type = "rectangle", LineWidth = 0.0025, DrawMode = "fill",
-						Position = { x = 0.795+((#Solo.Hints[#Solo.Hints]-1)%2)*0.0325, y = 0.12+math.floor(#Solo.Hints[#Solo.Hints]/2)*0.0325+(#Solo.Keypressed-1)*0.075},
-						Dimension = {width = 0.0225, height = 0.025}, Colors = {171, 171, 171, 255} })
-				end
-			end
+	-- Debug
+	if Debug == true then
+		for k,v in ipairs(Solo.Answer) do
+			print(k,v)
 		end
 	end
 
-	return found
+	Solo:resize() -- Call the function filling the canvas
+
+	Solo.Music = love.audio.newSource("Resources/BlueMind.ogg")
+	Solo.Music:setLooping(true)
+	Solo.Music:play()
+end
+
+function Solo:resize() -- Called in :enter() and when the window is resized
+	Solo.Display = love.graphics.newCanvas(Screen.width, Screen.height) -- Create the canvas containing base interface to avoid drawing it entirely each frame
+	love.graphics.setCanvas(Solo.Display)
+		love.graphics.setFont(Fonts[3]) -- Use font with size 3% by default
+		Solo.Display:clear()
+		for id, polygon in ipairs(GUI.InGame.Polygons) do
+			GUI.InGame.drawPolygon(polygon)
+		end
+	love.graphics.setCanvas()
 end
 
 function Solo:init()
+	Game.init()
 	GUI.InGame.loadInterface()
-	tmpanswer = { 1, 2, 3, 4, 5, 6 }
-
-	if not love.getVersion then -- Check if LÖVE is older than 0.9.1, getVersion() introduced in version 0.9.1
-		love.math.random() -- Throw first value since it's not random in LÖVE 0.9.0
-	end
-
-	while #Solo.Answer < 4 do
-		local n = love.math.random(#tmpanswer)
-		table.insert(Solo.Answer, tmpanswer[n])
-		table.remove(tmpanswer, n) -- table.insert(GUI.InGame.Polygons, { Type = "print", Text = n, Position = {x = 0.1+i*0.01, y = 0.5}, Colors = {255, 255, 255, 255}})
-	end
-
-	-- Debug
-	for k,v in ipairs(Solo.Answer) do
-		print(k,v)
-		table.insert(GUI.InGame.Polygons, { Type = "print", Text = v, Position = {x = 0.1+k*0.01, y = 0.5}, Colors = {255, 255, 255, 255}})
-	end
 end
 
 function Solo:draw()
-	for id, polygon in ipairs(GUI.InGame.Polygons) do
-		GUI.InGame.drawPolygon(polygon)
-	end
+	love.graphics.setColor(255, 255, 255, 255)
+	love.graphics.setBlendMode('premultiplied')
+	love.graphics.draw(Solo.Display)
+	love.graphics.setBlendMode('alpha')
 
 	for num, line in ipairs(Solo.Hints) do
 		for k, poly in ipairs(line) do
@@ -69,16 +64,32 @@ function Solo:draw()
 			GUI.InGame.drawPolygon({ Type = "rectangle", DrawMode = "fill", Position = { x = 0.50+(id-1)*0.075, y = 0.15+(line-1)*0.075-0.025}, Dimension = {width = 0.04, height = 0.04}, Colors = GUI.InGame.Colors[num] })
 		end
 	end
+
+	-- Debug
+	if Debug == true then
+		for id, num in ipairs(Solo.Answer) do
+			GUI.InGame.drawPolygon({ Type = "rectangle", DrawMode = "fill", Position = { x = 0.08+(id-1)*0.075+0.005, y = 0.12+0.005}, Dimension = {width = 0.035, height = 0.04}, Colors = GUI.InGame.Colors[num] }) -- Display answer
+		end
+	end
+
+	if Solo.state == "found" then
+		GUI.InGame.displayPopup("Found !")
+	end
+
+	GUI.InGame.drawPolygon({ Type = "print", Text = "Solo", Position = { x = 0.15, y = 0.06}, Colors = {0, 0, 171, 255} }) -- Display "Solo" text
+	GUI.InGame.drawPolygon({ Type = "print", Text = Config.player_name.text, Position = { x = 0.60, y = 0.06}, Colors = {0, 0, 171, 255} }) -- Display local player's name
 end
 
-function Solo:mousepressed(x, y, b)
-	x = x/Screen.width
+function Solo:mousepressed(x, y, b) -- Handle mouse
+	if Solo.state == "found" then return end -- Don't do anything if answer is already found
+
+	x = x/Screen.width -- relative coordinates
 	y = y/Screen.height
-	if 0.875 < y and y < 0.915 then
+	if 0.875 < y and y < 0.915 then -- lower part of the screen
 		if x > 0.45 then
 			for i = 0,5 do
 				if 0.50+i*0.075 < x and x < 0.50+i*0.075+0.04 then
-					if #Solo.Keypressed > 7 or #Solo.Keypressed[#Solo.Keypressed] >= 4  then
+					if #Solo.Keypressed > 7 or #Solo.Keypressed[#Solo.Keypressed] >= 4  then -- Line already full
 						return
 					else
 						for k,v in pairs(Solo.Keypressed[#Solo.Keypressed]) do
@@ -87,20 +98,26 @@ function Solo:mousepressed(x, y, b)
 							end
 						end
 
-						table.insert(Solo.Keypressed[#Solo.Keypressed], i+1)
+						table.insert(Solo.Keypressed[#Solo.Keypressed], i+1) -- Click detected on a color-button
 					end
 				end
 			end
-		elseif 0.25 < x and x < 0.375 and #Solo.Keypressed <= 7 and #Solo.Keypressed[#Solo.Keypressed] == 4 then
-			if Solo.checkKeys() then
-				table.insert(GUI.InGame.Polygons, { Type = "print", Text = "FOUND !", Position = {x = 0.5, y = 0.5}, Colors = {255, 255, 255, 255}})
+		elseif 0.25 < x and x < 0.375 and #Solo.Keypressed <= 7 and #Solo.Keypressed[#Solo.Keypressed] == 4 then -- Clik on Ok button
+			local hints = table.concat(Game.checkKeys(Solo.Keypressed[#Solo.Keypressed], Solo.Answer))
+			if hints == "1111" then
+				Solo.state = "found"
 			else
+				table.insert(Solo.Hints, Game.addHints(hints, #Solo.Keypressed))
 				Solo.Keypressed[#Solo.Keypressed+1] = {}
 			end
-		elseif #Solo.Keypressed <= 7 and 0.075 < x and x < 0.200 and #Solo.Keypressed[#Solo.Keypressed] > 0 then
+		elseif #Solo.Keypressed <= 7 and 0.075 < x and x < 0.200 and #Solo.Keypressed[#Solo.Keypressed] > 0 then -- Click on reset button
 			Solo.Keypressed[#Solo.Keypressed] = {}
 		end
 	end
 end
 
+function Solo:leave()
+	Solo.Music:stop()
+end
+
 return Solo
-- 
cgit v1.2.3-70-g09d2