Use session keys in the client to remove ambiguity

This commit is contained in:
Sasha 2018-04-12 23:01:40 +02:00
parent fab330b71d
commit 59f8751fb6
12 changed files with 355 additions and 237 deletions

View file

@ -7,4 +7,3 @@ class RW a b where
update :: (a -> a) -> b -> b update :: (a -> a) -> b -> b
set :: a -> b -> b set :: a -> b -> b
set = update . const set = update . const

View file

@ -7,11 +7,12 @@ import Network.HTTP.Types.Status (badRequest400)
import Network.WebSockets (defaultConnectionOptions) import Network.WebSockets (defaultConnectionOptions)
import Network.Wai.Handler.WebSockets (websocketsOr) import Network.Wai.Handler.WebSockets (websocketsOr)
import Network.Wai (responseLBS) import Network.Wai (responseLBS)
import Control.Monad.Reader (asks)
import qualified Config (listenPort) import qualified Config (listenPort)
import qualified Player (Login(..), T(..)) import qualified Player (Session(..), Status(..))
import qualified Server (logIn, logOut, disconnect) import qualified Server (logIn, logOut, disconnect, setStatus)
import qualified Session (App, debug, get, player, serve, update) import qualified Session (App, T(..), current, debug, get, serve, server, try, update)
import qualified Message (FromClient(..), T(..), broadcast, receive, relay, send) import qualified Message (FromClient(..), T(..), broadcast, receive, relay, send, sendTo)
type Vertex = Session.App () type Vertex = Session.App ()
type Edges = Message.FromClient -> Vertex type Edges = Message.FromClient -> Vertex
@ -27,7 +28,7 @@ newVertex name = do
enter :: Vertex enter :: Vertex
enter = do enter = do
Session.debug "Initial state" Session.debug "Initial state"
Session.get id >>= (Message.send . Message.Welcome) Message.Welcome <$> Session.server <*> asks Session.key >>= Message.send
connected connected
onErrorGoto :: Vertex -> String -> Session.App () onErrorGoto :: Vertex -> String -> Session.App ()
@ -38,7 +39,7 @@ connected :: Vertex
connected = newVertex "Connected" edges connected = newVertex "Connected" edges
where where
edges logIn@(Message.LogIn login) = edges logIn@(Message.LogIn login) =
Session.update (Server.logIn login) asks Session.key >>= Session.try . (Server.logIn login)
>>= maybe >>= maybe
(Message.relay logIn Message.broadcast >> loggedIn) (Message.relay logIn Message.broadcast >> loggedIn)
(onErrorGoto connected) (onErrorGoto connected)
@ -49,20 +50,37 @@ loggedIn = newVertex "Logged in" edges
where where
edges logOut@Message.LogOut = do edges logOut@Message.LogOut = do
Message.relay logOut Message.broadcast Message.relay logOut Message.broadcast
Session.update Server.logOut asks Session.key >>= Session.update . Server.logOut
>>= maybe
connected connected
(onErrorGoto loggedIn) edges invitation@(Message.Invitation {Message.to}) = do
session <- Session.get to
case Player.status session of
Player.LoggedIn True -> do
key <- asks Session.key
Session.update (Server.setStatus (Player.Waiting to) key)
Session.update (Server.setStatus (Player.Answering key) to)
(Message.relay invitation $ Message.sendTo (to, session))
loggedIn
_ -> onErrorGoto loggedIn "They just left"
edges (Message.Answer {Message.accept}) = do
current <- Session.current
case Player.status current of
Player.Answering to -> do
session <- Session.get to
key <- asks Session.key
case Player.status session of
Player.Waiting for | for == key ->
if accept
then Session.debug "Yeah ! Let's start a game" >> loggedIn
else Session.debug "Oh they said no" >> loggedIn
_ -> onErrorGoto loggedIn "They're not waiting for your answer"
_ -> onErrorGoto loggedIn "You haven't been invited yet"
edges _ = loggedIn edges _ = loggedIn
exit :: Vertex exit :: Vertex
exit = do exit = do
leaving <- Player.login <$> Session.player asks Session.key >>= Session.update . Server.disconnect
_ <- Session.update Server.disconnect -- ignoring never-occuring error Message.relay Message.LogOut Message.broadcast
case leaving of
Player.Login from -> Message.broadcast $
Message.Relay {Message.from, Message.message = Message.LogOut}
_ -> return ()
main :: IO () main :: IO ()
main = do main = do

View file

@ -7,23 +7,25 @@ module Message (
, receive , receive
, relay , relay
, send , send
, sendTo
) where ) where
import Data.Map (toList)
import Data.Aeson (FromJSON(..), ToJSON(..), eitherDecode', encode, genericParseJSON, genericToEncoding, defaultOptions) import Data.Aeson (FromJSON(..), ToJSON(..), eitherDecode', encode, genericParseJSON, genericToEncoding, defaultOptions)
import Network.WebSockets (receiveData, sendTextData) import Network.WebSockets (receiveData, sendTextData)
import Data.ByteString.Lazy.Char8 (unpack) import Data.ByteString.Lazy.Char8 (unpack)
import Control.Monad (mapM_) import Control.Monad (mapM_)
import Control.Monad.Reader (lift) import Control.Monad.Reader (asks, lift)
import qualified Player (Login(..), Name, T(..)) import qualified Player (Key, Name, Session(..))
import qualified Server (T(..)) import qualified Server (T(..))
import qualified Session (App, connection, debug, get, player) import qualified Session (App, T(..), connection, current, debug, server)
import qualified Hanafuda.KoiKoi as KoiKoi (Move(..)) import qualified Hanafuda.KoiKoi as KoiKoi (Move(..))
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Game () import Game ()
data FromClient = data FromClient =
Answer {accept :: Bool} Answer {accept :: Bool}
| Invitation {to :: Player.Name} | Invitation {to :: Player.Key}
| LogIn {name :: Player.Name} | LogIn {name :: Player.Name}
| LogOut | LogOut
| Game {move :: KoiKoi.Move} | Game {move :: KoiKoi.Move}
@ -36,8 +38,8 @@ instance FromJSON FromClient where
parseJSON = genericParseJSON defaultOptions parseJSON = genericParseJSON defaultOptions
data T = data T =
Relay {from :: Player.Name, message :: FromClient} Relay {from :: Player.Key, message :: FromClient}
| Welcome {room :: Server.T} | Welcome {room :: Server.T, key :: Player.Key}
| Pong | Pong
| Error {error :: String} | Error {error :: String}
deriving (Generic) deriving (Generic)
@ -45,31 +47,28 @@ data T =
instance ToJSON T where instance ToJSON T where
toEncoding = genericToEncoding defaultOptions toEncoding = genericToEncoding defaultOptions
sendTo :: T -> Player.T -> Session.App () sendTo :: (Player.Key, Player.Session) -> T -> Session.App ()
sendTo obj player = do sendTo (key, session) obj = do
Session.debug $ '(' : playerLogin ++ ") <" ++ (unpack encoded) Session.debug $ '(' : show key ++ ") <" ++ (unpack encoded)
lift $ sendTextData (Player.connection player) $ encoded lift $ sendTextData (Player.connection session) $ encoded
where where
encoded = encode $ obj encoded = encode $ obj
playerLogin = unpack $ encode $ Player.login player
send :: T -> Session.App () send :: T -> Session.App ()
send obj = send obj = do
(obj `sendTo`) =<< Session.player key <- asks Session.key
session <- Session.current
sendTo (key, session) obj
broadcast :: T -> Session.App () broadcast :: T -> Session.App ()
broadcast obj = broadcast obj =
Session.get Server.bySessionId (toList . Server.sessions) <$> Session.server
>>= mapM_ (obj `sendTo`) >>= mapM_ (flip sendTo obj)
relay :: FromClient -> (T -> Session.App ()) -> Session.App () relay :: FromClient -> (T -> Session.App ()) -> Session.App ()
relay message f = relay message f = do
Session.debug "Relaying" Session.debug "Relaying"
>> Session.player >>= (ifLoggedIn . Player.login) (\from -> f $ Relay {from, message}) =<< asks Session.key
>> Session.debug "Relayed"
where
ifLoggedIn Player.Anonymous = return ()
ifLoggedIn (Player.Login from) = f $ Relay {from, message}
receive :: Session.App FromClient receive :: Session.App FromClient
receive = do receive = do

View file

@ -1,70 +1,72 @@
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
module Player ( module Player (
Login(..) Key(..)
, Name(..) , Name
, Session(..)
, Status(..) , Status(..)
, T(..) , T(..)
, new , new
, openSession
) where ) where
import Data.Monoid ((<>)) import Data.Text (Text, pack)
import Data.Text (Text) import Data.Aeson (FromJSON(..), ToJSON(..), ToJSONKey(..), genericToEncoding)
import Data.Aeson (FromJSON(..), ToJSON(..), (.=), Value(..), genericToEncoding, object, pairs) import Data.Aeson.Types (toJSONKeyText)
import qualified JSON (defaultOptions, singleLCField) import qualified JSON (defaultOptions)
import qualified Data (RW(..)) import qualified Data (RW(..))
import Network.WebSockets (Connection) import Network.WebSockets (Connection)
import GHC.Generics import GHC.Generics
newtype Key = Key Int deriving (Eq, Ord, Read, Show, Generic)
newtype Name = Name Text deriving (Eq, Ord, Generic) newtype Name = Name Text deriving (Eq, Ord, Generic)
data Login = Anonymous | Login Name
data T = T {
key :: Key
, name :: Name
}
deriving (Generic)
instance Data.RW Key T where
update f player@(T {key}) = player {key = f key}
instance Data.RW Name T where
update f player@(T {name}) = player {name = f name}
instance FromJSON Key
instance ToJSON Key where
toEncoding = genericToEncoding JSON.defaultOptions
instance ToJSONKey Key where
toJSONKey = toJSONKeyText (pack . \(Key n) -> show n)
instance FromJSON Name
instance ToJSON Name where
toEncoding = genericToEncoding JSON.defaultOptions
instance ToJSON T where
toEncoding = genericToEncoding JSON.defaultOptions
data Status = data Status =
LoggedIn Bool LoggedIn Bool
| Answering Name | Answering Key
| Waiting Name | Waiting Key
deriving (Generic) deriving (Generic)
data T = T { data Session = Session {
connection :: Connection connection :: Connection
, login :: Login
, status :: Status , status :: Status
} }
instance Data.RW Login T where instance Data.RW Status Session where
update f player@(T {login}) = player {login = f login} update f session@(Session {status}) = session {status = f status}
instance Data.RW Status T where new :: Key -> Name -> T
update f player@(T {status}) = player {status = f status} new key name = T {key, name}
instance ToJSON Name where openSession :: Connection -> Session
toEncoding = genericToEncoding JSON.defaultOptions openSession connection = Session {
instance FromJSON Name connection
, status = LoggedIn False
instance ToJSON Login where }
toJSON Anonymous = toJSON Null
toJSON (Login name) = toJSON name
toEncoding Anonymous = toEncoding Null
toEncoding (Login name) = toEncoding name
instance FromJSON Login where
parseJSON Null = return Anonymous
parseJSON s = Login <$> parseJSON s
instance ToJSON Status where
toEncoding = genericToEncoding JSON.singleLCField
instance ToJSON T where
toJSON (T {login, status}) = object ["login" .= login, "status" .= status]
toEncoding (T {login, status}) = pairs (
"login" .= login <> "status" .= status
)
new :: Connection -> T
new connection = T {connection, login = Anonymous, status = LoggedIn False}

View file

@ -2,71 +2,79 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DeriveGeneric #-}
module Server ( module Server (
SessionId T(..)
, T(..)
, disconnect , disconnect
, join , join
, logIn , logIn
, logOut , logOut
, new , new
, setStatus
) where ) where
import Data.Vector (fromList) import Data.Map ((!?), Map, adjust, delete, insert, lookupMax)
import Data.Aeson (ToJSON(..), Value(Array)) import qualified Data.Map as Map (empty)
import Data.Map ((!), (!?), Map, adjust, delete, elems, empty, insert, lookupMax) import Data.Aeson (ToJSON(..))
import qualified Data (RW(..)) import qualified Data (RW(..))
import qualified Player (Login(..), Name(..), T(..)) import qualified Player (Key(..), Name, Session(..), Status(..))
newtype SessionId = SessionId Int deriving (Eq, Ord, Read, Show) type Keys = Map Player.Name Player.Key
type Players = Map SessionId Player.T type Names = Map Player.Key Player.Name
type SessionIds = Map Player.Name SessionId type Sessions = Map Player.Key Player.Session
data T = T { data T = T {
byName :: SessionIds keys :: Keys
, bySessionId :: Players , names :: Names
, sessions :: Sessions
} }
instance Data.RW SessionIds T where instance Data.RW Keys T where
update f server@(T {byName}) = server {byName = f byName} update f server@(T {keys}) = server {keys = f keys}
instance Data.RW Players T where instance Data.RW Names T where
update f server@(T {bySessionId}) = server {bySessionId = f bySessionId} update f server@(T {names}) = server {names = f names}
loggedInPlayers :: T -> [Player.T] instance Data.RW Sessions T where
loggedInPlayers (T {byName, bySessionId}) = update f server@(T {sessions}) = server {sessions = f sessions}
[(bySessionId ! sessionId) | sessionId <- elems byName]
instance ToJSON T where instance ToJSON T where
toJSON = Array . fromList . (toJSON <$>) . loggedInPlayers toJSON = toJSON . names
toEncoding = toEncoding . loggedInPlayers toEncoding = toEncoding . names
new :: T new :: T
new = T { new = T {
byName = empty keys = Map.empty
, bySessionId = empty , names = Map.empty
, sessions = Map.empty
} }
join :: Player.T -> T -> IO (T, SessionId) join :: Player.Session -> T -> IO (T, Player.Key)
join player server@(T {bySessionId}) = join session server@(T {sessions}) =
return (Data.update (insert sessionId player) server, sessionId) return (Data.update (insert key session) server, key)
where where
sessionId = SessionId $ maybe 0 (\(SessionId n, _) -> n+1) $ lookupMax bySessionId key = Player.Key $ maybe 0 (\(Player.Key n, _) -> n+1) $ lookupMax sessions
disconnect :: SessionId -> T -> Either String T disconnect :: Player.Key -> T -> T
disconnect sessionId server = disconnect key =
Data.update (delete sessionId :: Players -> Players) <$> logOut sessionId server Data.update (delete key :: Sessions -> Sessions) . logOut key
logIn :: Player.Name -> SessionId -> T -> Either String T logIn :: Player.Name -> Player.Key -> T -> Either String T
logIn name sessionId server = logIn name key server =
Data.update (adjust (Data.set (Player.Login name) :: Player.T -> Player.T) sessionId) <$> Data.update (insert name key) .
Data.update (insert name sessionId) <$> Data.update (insert key name) .
maybe (Right server) (\_ -> Left "This name is already registered") maybeName setStatus (Player.LoggedIn True) key <$>
where maybe (Right server) (\_-> Left "This name is already registered") (keys server !? name)
maybeName = byName server !? name
logOut :: SessionId -> T -> Either String T logOut :: Player.Key -> T -> T
logOut sessionId server@(T {bySessionId}) = logOut key server =
Right $ Data.update (adjust (Data.set Player.Anonymous :: Player.T -> Player.T) sessionId) $ maybe
(case Player.login $ bySessionId ! sessionId of server
(Player.Login name) -> Data.update (delete name :: SessionIds -> SessionIds) server (\name ->
Player.Anonymous -> server) Data.update (delete key :: Names -> Names) $
setStatus (Player.LoggedIn False) key $
Data.update (delete name :: Keys -> Keys) server)
(names server !? key)
setStatus :: Player.Status -> Player.Key -> T -> T
setStatus status key =
Data.update (adjust (Data.set status) key :: Sessions -> Sessions)

View file

@ -5,8 +5,10 @@ module Session (
, connection , connection
, debug , debug
, get , get
, player , current
, serve , serve
, server
, try
, update , update
) where ) where
@ -15,50 +17,54 @@ import Control.Concurrent (MVar, newMVar, modifyMVar, putMVar, readMVar, takeMVa
import Control.Exception (finally) import Control.Exception (finally)
import Control.Monad.Reader (ReaderT(..), ask, asks, lift) import Control.Monad.Reader (ReaderT(..), ask, asks, lift)
import Network.WebSockets (Connection, ServerApp, acceptRequest) import Network.WebSockets (Connection, ServerApp, acceptRequest)
import qualified Player (T(..), new) import qualified Player (Key, Session(..), openSession)
import qualified Server (SessionId, T(..), join, new) import qualified Server (T(..), join, new)
data T = T { data T = T {
server :: MVar Server.T mServer :: MVar Server.T
, key :: Server.SessionId , key :: Player.Key
} }
type App a = ReaderT T IO a type App a = ReaderT T IO a
get :: (Server.T -> a) -> App a server :: App Server.T
get f = server = asks mServer >>= lift . readMVar
asks server
>>= lift . (f <$>) . readMVar
player :: App Player.T get :: Player.Key -> App Player.Session
player = do get key =
sId <- asks key (! key) . Server.sessions <$> server
get ((! sId) . Server.bySessionId)
current :: App Player.Session
current = do
asks key >>= get
connection :: App Connection connection :: App Connection
connection = Player.connection <$> player connection = Player.connection <$> current
debug :: String -> App () debug :: String -> App ()
debug message = debug message =
show <$> asks Session.key show <$> asks key
>>= lift . putStrLn . (++ ' ':message) >>= lift . putStrLn . (++ ' ':message)
update :: (Server.SessionId -> Server.T -> Either String Server.T) -> App (Maybe String) try :: (Server.T -> Either String Server.T) -> App (Maybe String)
update f = do try f = do
T {server, key} <- ask T {mServer} <- ask
currentValue <- lift $ takeMVar server currentValue <- lift $ takeMVar mServer
lift $ case f key currentValue of lift $ case f currentValue of
Left message -> putMVar server currentValue >> return (Just message) Left message -> putMVar mServer currentValue >> return (Just message)
Right updated -> putMVar server updated >> return Nothing Right updated -> putMVar mServer updated >> return Nothing
update :: (Server.T -> Server.T) -> App ()
update f = try (Right . f) >> return ()
serve :: App () -> App () -> IO ServerApp serve :: App () -> App () -> IO ServerApp
serve onEnter onExit = do serve onEnter onExit = do
server <- newMVar Server.new mServer <- newMVar Server.new
return $ \pending -> do return $ \pending -> do
key <- acceptRequest pending key <- acceptRequest pending
>>= return . Player.new >>= return . Player.openSession
>>= modifyMVar server . Server.join >>= modifyMVar mServer . Server.join
finally finally
(runReaderT onEnter $ T {server, key}) (runReaderT onEnter $ T {mServer, key})
(runReaderT onExit $ T {server, key}) (runReaderT onExit $ T {mServer, key})

View file

@ -1,7 +1,8 @@
window.addEventListener('load', function() { window.addEventListener('load', function() {
var ws = new WebSocket('ws://' + window.location.hostname + '/play/'); var ws = new WebSocket('ws://' + window.location.hostname + '/play/');
var sessionKey = null;
var lib = Lib(ws); var lib = Lib(ws);
var room = Room(document.getElementById('players'), lib); var room = Room(document.getElementById('room'), lib);
var login = Login(document.getElementById('login'), lib); var login = Login(document.getElementById('login'), lib);
var debug = document.getElementById('debug'); var debug = document.getElementById('debug');
setTimeout(ping, 20000); setTimeout(ping, 20000);
@ -10,7 +11,8 @@ window.addEventListener('load', function() {
var o = JSON.parse(event.data); var o = JSON.parse(event.data);
switch(o.tag) { switch(o.tag) {
case "Welcome": case "Welcome":
room.populate(o.room); sessionKey = o.key;
room.populate(o.room, sessionKey);
break; break;
case "Pong": case "Pong":
setTimeout(ping, 10000); setTimeout(ping, 10000);
@ -26,13 +28,25 @@ window.addEventListener('load', function() {
function relayedMessage(o) { function relayedMessage(o) {
switch(o.message.tag) { switch(o.message.tag) {
case "LogIn": case "LogIn":
room.enter(o.from); room.enter(o.from, o.message.name);
login.onLogIn(o.from); if(o.from == sessionKey) {
login.on(o.from);
}
break; break;
case "LogOut": case "LogOut":
room.leave(o.from); room.leave(o.from);
login.onLogOut(o.from); if(o.from == sessionKey) {
login.off(o.from);
}
break; break;
case "Invitation":
var name = room.name(o.from);
var accept = false;
// invitations should come only from known players, in doubt say «no»
if(name) {
accept = confirm(name + " has invited you to a game");
}
lib.send({tag: "Answer", accept: accept});
} }
} }

View file

@ -19,9 +19,10 @@
<input type="button" name="leave" value="Leave"/> <input type="button" name="leave" value="Leave"/>
</p> </p>
</form> </form>
<form id="room"> <form id="room" class="off">
<ul id="players"> <ul class="players">
</ul> </ul>
<input type="number" hidden name="guest"/>
<input type="submit" name="invite" value="Invite to a game" disabled/> <input type="submit" name="invite" value="Invite to a game" disabled/>
</form> </form>
<p id="debug"></p> <p id="debug"></p>

View file

@ -12,8 +12,8 @@ function Lib(ws) {
} }
function insert(obj, t, min, max) { function insert(obj, t, min, max) {
min = min || 0; min = min == undefined ? 0 : min;
max = max || t.length; max = max == undefined ? t.length : max;
if(max - min < 1) { if(max - min < 1) {
return min; return min;
} }

View file

@ -1,5 +1,4 @@
function Login(domElem, lib) { function Login(domElem, lib) {
var login = null;
domElem.addEventListener('submit', function(e) { domElem.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
lib.send({tag: "LogIn", name: domElem.name.value}) lib.send({tag: "LogIn", name: domElem.name.value})
@ -10,21 +9,15 @@ function Login(domElem, lib) {
}); });
return { return {
onLogIn: onLogIn, on: on,
onLogOut: onLogOut off: off
}; };
function onLogIn(name) { function on(name) {
if(name == domElem.name.value) {
domElem.className = "on"; domElem.className = "on";
login = name;
}
} }
function onLogOut(name) { function off() {
if(name == login) {
login = null;
domElem.className = ""; domElem.className = "";
} }
}
} }

View file

@ -1,40 +1,109 @@
function Room(domElem, lib) { function Room(domElem, lib) {
var players = {}; var players = {};
var keys = {};
var logins = []; var logins = [];
var session = {
key: null,
loggedIn: false,
selected: null
};
var playersList = domElem.getElementsByClassName('players')[0];
domElem.addEventListener('submit', function(e) {
e.preventDefault();
lib.send({tag: "Invitation", to: parseInt(domElem.guest.value)})
});
return { return {
populate: populate, populate: populate,
enter: enter, enter: enter,
leave: leave leave: leave,
name: name
}; };
function Player(name) { function Player(key, name) {
var player = { var player = {
name: name,
dom: document.createElement('li'), dom: document.createElement('li'),
position: null position: null
}; };
player.dom.textContent = name; player.dom.textContent = name;
if(key != session.key) {
player.dom.addEventListener('click', function(e) {
e.preventDefault();
if(session.loggedIn) {
select(key);
}
});
} else {
on();
player.dom.title = "Hey ! That's you !";
}
return player; return player;
} }
function populate(playersList) { function populate(playersHash, sessionKey) {
lib.clearElement(domElem); session.key = sessionKey;
for(var i = 0; i < playersList.length; i++) { lib.clearElement(playersList);
enter(playersList[i].login || "anon"); for(var key in playersHash) {
enter(key, playersHash[key] || "anon");
} }
} }
function enter(name) { function enter(key, name) {
var player = Player(name); var player = Player(key, name);
players[name] = player; keys[key] = name;
players[key] = player;
player.position = lib.insert(name, logins); player.position = lib.insert(name, logins);
beforePlayer = logins[player.position]; beforePlayer = players[keys[logins[player.position]]];
domElem.insertBefore(player.dom, beforePlayer && players[beforePlayer].dom); playersList.insertBefore(player.dom, beforePlayer && beforePlayer.dom);
logins.splice(player.position, 0, name); logins.splice(player.position, 0, name);
} }
function leave(name) { function leave(key) {
domElem.removeChild(players[name].dom); var player = players[key];
logins.splice(players[name].position, 1); if(key == session.key) {
off();
}
if(player != undefined) {
playersList.removeChild(player.dom);
logins.splice(player.position, 1);
delete keys[player.name]
delete players[key];
}
}
function name(key) {
player = players[key];
return player && player.name;
}
function on() {
domElem.className = "";
session.loggedIn = true;
}
function off() {
domElem.className = "off";
session.loggedIn = false;
}
function select(key) {
if(key == domElem.guest.value) {
unselect(players[key].dom);
} else {
if(session.selected) {
unselect(session.selected);
}
players[key].dom.className = "selected";
session.selected = players[key].dom;
domElem.guest.value = key;
domElem.invite.disabled = false;
}
}
function unselect(dom) {
dom.className = "";
domElem.guest.value = "";
domElem.invite.disabled = true;
} }
} }

View file

@ -10,10 +10,19 @@
display: inline; display: inline;
} }
#players { #room .players {
min-height: 4em; min-height: 4em;
border: 1px solid #ccc; border: 1px solid #ccc;
list-style: none; list-style: none;
padding-left: 0; padding-left: 0;
cursor: pointer; cursor: pointer;
} }
#room.off .players li {
color: #777;
}
#room .players .selected {
background: #92c8f6;
color: #fff;
}