Compare commits

..

3 commits

24 changed files with 514 additions and 473 deletions

4
.gitignore vendored
View file

@ -1,3 +1 @@
/dist* /dist/*
cabal.project.local
.ghc*

View file

@ -1,18 +1,5 @@
# Revision history for hanafudapi # Revision history for hanafudapi
## 0.2.3.0 -- 2019-08-24
* Huge refactoring to use the new APILanguage that basically vampirized Game module which become more of a toolbox for the Automaton
* Fix a couple race conditions in JS client and server encountered when developping and testing Hannah the bot soon to come
## 0.2.2.0 -- 2019-08-12
* Handle the end of games
## 0.2.1.0 -- 2019-01-08
* Use latest changes in the lib to send a log of what happened during a turn
## 0.2.0.1 -- 2018-08-26 ## 0.2.0.1 -- 2018-08-26
* Games are now playable * Games are now playable

View file

@ -2,14 +2,14 @@
-- documentation, see http://haskell.org/cabal/users-guide/ -- documentation, see http://haskell.org/cabal/users-guide/
name: hanafuda-webapp name: hanafuda-webapp
version: 0.2.3.0 version: 0.2.1.0
synopsis: A webapp for the Haskell hanafuda library synopsis: A webapp for the Haskell hanafuda library
-- description: -- description:
homepage: https://git.marvid.fr/hanafuda homepage: https://framagit.org/hanafuda
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Tissevert author: Sasha
maintainer: tissevert+devel@marvid.fr maintainer: sasha+frama@marvid.fr
-- copyright: -- copyright:
category: Web category: Web
build-type: Simple build-type: Simple
@ -17,25 +17,26 @@ extra-source-files: ChangeLog.md
cabal-version: >=1.10 cabal-version: >=1.10
source-repository head source-repository head
type: git type: git
location: https://git.marvid.fr/hanafuda/webapp location: https://framagit.org/hanafuda/api
executable hanafudapi executable hanafudapi
main-is: Main.hs main-is: Main.hs
other-modules: App other-modules: App
, Automaton , Automaton
, Config , Config
, Messaging , Message
, Game , Game
, RW , JSON
, Data
, Player
, Server , Server
, Session , Session
-- other-extensions: -- other-extensions:
build-depends: base >=4.9 && <4.13 build-depends: base >=4.10 && <4.13
, bytestring , bytestring
, containers >= 0.5.9 , containers
, unordered-containers , unordered-containers
, hanafuda >= 0.3.3 , hanafuda >= 0.3.0
, hanafuda-APILanguage >= 0.1.0
, http-types , http-types
, aeson , aeson
, mtl , mtl

View file

@ -12,17 +12,17 @@ module App (
, update_ , update_
) where ) where
import Data.Map ((!))
import Control.Concurrent (MVar, modifyMVar, putMVar, readMVar, takeMVar) import Control.Concurrent (MVar, modifyMVar, putMVar, readMVar, takeMVar)
import Control.Monad.Reader (ReaderT(..), ask, asks, lift) import Control.Monad.Reader (ReaderT(..), ask, asks, lift)
import Data.Map ((!))
import Hanafuda.KoiKoi (PlayerID)
import Network.WebSockets (Connection) import Network.WebSockets (Connection)
import qualified Server (T(..)) import qualified Player (Key)
import qualified Session (T(..)) import qualified Session (T(..))
import qualified Server (T(..))
data Context = Context { data Context = Context {
mServer :: MVar Server.T mServer :: MVar Server.T
, playerID :: PlayerID , key :: Player.Key
} }
type T a = ReaderT Context IO a type T a = ReaderT Context IO a
@ -30,20 +30,20 @@ type T a = ReaderT Context IO a
server :: T Server.T server :: T Server.T
server = asks mServer >>= lift . readMVar server = asks mServer >>= lift . readMVar
get :: PlayerID -> T Session.T get :: Player.Key -> T Session.T
get playerID = get key =
(! playerID) . Server.sessions <$> server (! key) . Server.sessions <$> server
current :: T Session.T current :: T Session.T
current = do current = do
asks playerID >>= get asks key >>= get
connection :: T Connection connection :: T Connection
connection = Session.connection <$> current connection = Session.connection <$> current
debug :: String -> T () debug :: String -> T ()
debug message = debug message =
show <$> asks playerID show <$> asks key
>>= lift . putStrLn . (++ ' ':message) >>= lift . putStrLn . (++ ' ':message)
try :: (Server.T -> Either String Server.T) -> T (Maybe String) try :: (Server.T -> Either String Server.T) -> T (Maybe String)

View file

@ -3,102 +3,95 @@ module Automaton (
start start
) where ) where
import qualified App (Context(..), T, current, debug, get, server, try, update_) import Control.Monad.Except (runExceptT)
import Control.Monad.Reader (asks) import Control.Monad.Reader (asks, lift)
import Data.Map (Map, (!?)) import Control.Monad.Writer (runWriterT)
import qualified Data (RW(..))
import qualified Game (new, play) import qualified Game (new, play)
import qualified Hanafuda.KoiKoi as KoiKoi ( import qualified Hanafuda.KoiKoi as KoiKoi (Game(..), players)
Game, GameBlueprint(..), GameID, Step(..) import qualified Hanafuda.Player as Player (next)
)
import qualified Hanafuda.Message as Message (FromClient(..), T(..))
import qualified Messaging (
broadcast, get, notifyPlayers, relay, send, sendTo, update
)
import qualified RW (RW(..))
import qualified Server (endGame, get, logIn, logOut, update, room)
import qualified Session (Status(..), T(..), Update) import qualified Session (Status(..), T(..), Update)
import qualified Server (endGame, get, logIn, logOut, update, register)
import qualified App (Context(..), T, current, debug, get, server, try, update, update_)
import qualified Message (FromClient(..), T(..), broadcast, get, notifyPlayers, relay, send, sendTo, update)
receive :: Session.Status -> Message.FromClient -> App.T () receive :: Session.Status -> Message.FromClient -> App.T ()
receive (Session.LoggedIn False) logIn@(Message.LogIn login) = receive (Session.LoggedIn False) logIn@(Message.LogIn login) =
asks App.playerID >>= App.try . (Server.logIn login) asks App.key >>= App.try . (Server.logIn login)
>>= maybe >>= maybe
(Messaging.relay logIn Messaging.broadcast >> setSessionStatus (Session.LoggedIn True)) (Message.relay logIn Message.broadcast >> move (Session.LoggedIn True))
sendError sendError
receive (Session.LoggedIn True) logOut@Message.LogOut = do receive (Session.LoggedIn True) logOut@Message.LogOut = do
Messaging.relay logOut Messaging.broadcast Message.relay logOut Message.broadcast
asks App.playerID >>= App.update_ . Server.logOut asks App.key >>= App.update_ . Server.logOut
setSessionStatus (Session.LoggedIn False) move (Session.LoggedIn False)
receive (Session.LoggedIn True) invitation@(Message.Invitation {Message.to}) = do receive (Session.LoggedIn True) invitation@(Message.Invitation {Message.to}) = do
session <- App.get to session <- App.get to
case Session.status session of case Session.status session of
Session.LoggedIn True -> do Session.LoggedIn True -> do
from <- asks App.playerID key <- asks App.key
App.update_ (Server.update to (RW.set $ Session.Answering from :: Session.Update)) App.update_ (Server.update to (Data.set $ Session.Answering key :: Session.Update))
Messaging.broadcast $ Messaging.update {Message.paired = [from, to]} Message.broadcast $ Message.update {Message.paired = [key, to]}
(Messaging.relay invitation $ Messaging.sendTo [to]) (Message.relay invitation $ Message.sendTo [to])
setSessionStatus (Session.Waiting to) move (Session.Waiting to)
_ -> sendError "They just left" _ -> sendError "They just left"
receive (Session.Answering to) message@(Message.Answer {Message.accept}) = do receive (Session.Answering to) message@(Message.Answer {Message.accept}) = do
session <- App.get to session <- App.get to
playerID <- asks App.playerID key <- asks App.key
case Session.status session of case Session.status session of
Session.Waiting for | for == playerID -> do Session.Waiting for | for == key -> do
Messaging.relay message $ Messaging.sendTo [to] Message.relay message $ Message.sendTo [to]
newStatus <- newStatus <-
if accept if accept
then do then do
gameID <- Game.new (for, to) gameKey <- Server.register <$> (lift $ Game.new for to) >>= App.update
game <- Server.get gameID <$> App.server game <- Server.get gameKey <$> App.server
Messaging.notifyPlayers game [] Message.notifyPlayers game []
return $ Session.Playing gameID return $ Session.Playing gameKey
else do else do
Messaging.broadcast $ Messaging.update {Message.alone = [for, to]} Message.broadcast $ Message.update {Message.alone = [key, to]}
return $ Session.LoggedIn True return $ Session.LoggedIn True
App.update_ $ Server.update to (RW.set newStatus :: Session.Update) App.update_ $ Server.update to (Data.set newStatus :: Session.Update)
setSessionStatus newStatus move newStatus
_ -> sendError "They're not waiting for your answer" _ -> sendError "They're not waiting for your answer"
receive (Session.Playing gameID) played@(Message.Play {}) = do receive (Session.Playing gameKey) played@(Message.Play {}) = do
playerID <- asks App.playerID key <- asks App.key
game <- Server.get gameID <$> App.server game <- Server.get gameKey <$> App.server
(result, logs) <- Game.play playerID (Message.move played) game (result, logs) <- lift . runWriterT . runExceptT $ Game.play key (Message.move played) game
case result of case result of
Left message -> sendError message Left message -> sendError message
Right newGame -> do Right newGame ->
case KoiKoi.step newGame of if KoiKoi.on newGame
KoiKoi.Over -> do then do
App.debug $ "Game " ++ show gameID ++ " ended" App.update_ $ Server.update gameKey (const newGame)
App.update_ $ Server.endGame gameID Message.notifyPlayers newGame logs
_ -> App.update_ $ Server.update gameID (const newGame) else do
Messaging.notifyPlayers newGame logs let newStatus = Session.LoggedIn True
let opponent = Player.next (KoiKoi.players newGame) key
receive (Session.Playing gameID) Message.Quit = do App.update_ $ Server.endGame gameKey
games <- (RW.get <$> App.server :: App.T (Map KoiKoi.GameID KoiKoi.Game)) App.update_ $ Server.update opponent (Data.set newStatus :: Session.Update)
case games !? gameID of Message.notifyPlayers newGame logs
Nothing -> do move newStatus
playerID <- asks App.playerID
Messaging.broadcast $ Messaging.update {Message.alone = [playerID]}
setSessionStatus (Session.LoggedIn True)
_ -> sendError "Game is still running"
receive state _ = sendError $ "Invalid message in state " ++ show state receive state _ = sendError $ "Invalid message in state " ++ show state
sendError :: String -> App.T () sendError :: String -> App.T ()
sendError = Messaging.send . Message.Error sendError = Message.send . Message.Error
setSessionStatus :: Session.Status -> App.T () move :: Session.Status -> App.T ()
setSessionStatus newStatus = do move newStatus = do
playerID <- asks App.playerID key <- asks App.key
App.update_ $ Server.update playerID $ (RW.set newStatus :: Session.Update) App.update_ $ Server.update key $ (Data.set newStatus :: Session.Update)
App.debug $ show newStatus App.debug $ show newStatus
loop :: App.T () loop :: App.T ()
loop = do loop = do
message <- Messaging.get message <- Message.get
status <- Session.status <$> App.current status <- Session.status <$> App.current
status `receive` message status `receive` message
loop loop
@ -106,5 +99,5 @@ loop = do
start :: App.T () start :: App.T ()
start = do start = do
App.debug "Initial state" App.debug "Initial state"
Message.Welcome . Server.room <$> App.server <*> asks App.playerID >>= Messaging.send Message.Welcome <$> App.server <*> asks App.key >>= Message.send
loop loop

30
src/Data.hs Normal file
View file

@ -0,0 +1,30 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveGeneric #-}
module Data (
Key(..)
, RW(..)
) where
import Data.Aeson (FromJSON(..), ToJSON(..), ToJSONKey(..), genericToEncoding)
import Data.Aeson.Types (toJSONKeyText)
import Data.Text (pack)
import GHC.Generics
import qualified JSON (defaultOptions)
class RW a b where
get :: b -> a
set :: a -> b -> b
update :: (a -> a) -> b -> b
update f v =
set (f (get v)) v
newtype Key a = Key Int deriving (Eq, Ord, Enum, Read, Show, Generic)
instance FromJSON (Key a)
instance ToJSON (Key a) where
toEncoding = genericToEncoding JSON.defaultOptions
instance ToJSONKey (Key a) where
toJSONKey = toJSONKeyText (pack . \(Key n) -> show n)

View file

@ -1,40 +1,112 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
module Game ( module Game (
export Key
, T
, export
, new , new
, play , play
) where ) where
import qualified App (T, update) import Control.Monad.Except (throwError)
import Control.Monad.Except (runExceptT, throwError) import Data.Text (pack)
import Control.Monad.Reader (lift)
import Control.Monad.Writer (runWriterT)
import Data.Map (mapWithKey) import Data.Map (mapWithKey)
import qualified Hanafuda (empty) import Data.HashMap.Strict (insert)
import Hanafuda.KoiKoi (Game, GameBlueprint(..), GameID, Mode(..), PlayerID) import Data.Aeson (FromJSON(..), ToJSON(..), ToJSON1(..), ToJSONKey(..), Value(..), genericParseJSON, genericToEncoding, genericLiftToEncoding, toEncoding1, toJSON1)
import qualified Hanafuda.KoiKoi as KoiKoi ( import Data.Aeson.Types (toJSONKeyText)
Action, Move(..), play, new import qualified JSON (defaultOptions, singleLCField)
) import qualified Data (Key)
import Hanafuda.Message (PublicGame) import qualified Player (Key)
import qualified Hanafuda (Flower(..), Card(..), Pack, cardsOfPack, empty)
import qualified Hanafuda.Player (Player(..), Players(..)) import qualified Hanafuda.Player (Player(..), Players(..))
import qualified Server (register) import qualified Hanafuda.KoiKoi (Action(..), Game(..), Environment, Mode(..), Move(..), Score, Source(..), Step(..), Yaku(..), new, play)
import GHC.Generics
new :: (PlayerID, PlayerID) -> App.T GameID deriving instance Generic Hanafuda.Card
new (for, to) = deriving instance Generic Hanafuda.Flower
Server.register <$> (lift $ KoiKoi.new (for, to) WholeYear) >>= App.update deriving instance Generic Hanafuda.KoiKoi.Action
deriving instance Generic Hanafuda.KoiKoi.Mode
deriving instance Generic Hanafuda.KoiKoi.Move
deriving instance Generic Hanafuda.KoiKoi.Yaku
deriving instance Generic Hanafuda.KoiKoi.Source
deriving instance Generic Hanafuda.KoiKoi.Step
deriving instance Generic1 (Hanafuda.Player.Player Player.Key)
deriving instance Generic1 (Hanafuda.Player.Players Player.Key)
export :: PlayerID -> Game -> PublicGame type T = Hanafuda.KoiKoi.Game Player.Key
export playerID game = game {
deck = length $ deck game deriving instance Generic T
, players = Hanafuda.Player.Players $ mapWithKey maskOpponentsHand unfiltered
} instance ToJSON T where
toEncoding = genericToEncoding JSON.defaultOptions
instance FromJSON Hanafuda.Card
instance ToJSON Hanafuda.Card
instance ToJSON Hanafuda.Flower
instance ToJSON Hanafuda.Pack where
toJSON = toJSON . Hanafuda.cardsOfPack
toEncoding = toEncoding . Hanafuda.cardsOfPack
instance ToJSON Hanafuda.KoiKoi.Action
instance ToJSON Hanafuda.KoiKoi.Mode
instance FromJSON Hanafuda.KoiKoi.Move where
parseJSON = genericParseJSON JSON.singleLCField
instance ToJSON Hanafuda.KoiKoi.Move where
toEncoding = genericToEncoding JSON.singleLCField
instance ToJSON Hanafuda.KoiKoi.Source
instance ToJSON Hanafuda.KoiKoi.Step where
toEncoding = genericToEncoding JSON.defaultOptions
instance ToJSON1 (Hanafuda.Player.Player Player.Key) where
liftToEncoding = genericLiftToEncoding JSON.defaultOptions
instance ToJSON (Hanafuda.Player.Player Player.Key Hanafuda.KoiKoi.Score) where
toJSON = toJSON1
toEncoding = toEncoding1
instance ToJSON Hanafuda.KoiKoi.Yaku where
toEncoding = genericToEncoding JSON.defaultOptions
instance ToJSONKey Hanafuda.KoiKoi.Yaku where
toJSONKey = toJSONKeyText (pack . show)
instance ToJSON1 (Hanafuda.Player.Players Player.Key) where
liftToEncoding = genericLiftToEncoding JSON.defaultOptions
instance ToJSON (Hanafuda.Player.Players Player.Key Hanafuda.KoiKoi.Score) where
toJSON = toJSON1
toEncoding = toEncoding1
type Key = Data.Key T
new :: Player.Key -> Player.Key -> IO T
new p1 p2 = do
Hanafuda.KoiKoi.new [p1, p2] $ Hanafuda.KoiKoi.FirstAt 1
export :: Player.Key -> T -> Value
export key game = Object $ insert "deck" (toJSON $ length $ Hanafuda.KoiKoi.deck game) $ ast
where where
Hanafuda.Player.Players unfiltered = Hanafuda.KoiKoi.players game Hanafuda.Player.Players unfiltered = Hanafuda.KoiKoi.players game
maskOpponentsHand k player maskOpponentsHand k player
| k == playerID = player | k == key = player
| otherwise = player {Hanafuda.Player.hand = Hanafuda.empty} | otherwise = player {Hanafuda.Player.hand = Hanafuda.empty}
Object ast = toJSON $ game {
Hanafuda.KoiKoi.players = Hanafuda.Player.Players $ mapWithKey maskOpponentsHand unfiltered
}
play :: PlayerID -> KoiKoi.Move -> Game -> App.T (Either String Game, [KoiKoi.Action]) play :: Hanafuda.KoiKoi.Environment m => Player.Key -> Hanafuda.KoiKoi.Move -> T -> m (Hanafuda.KoiKoi.Game Player.Key)
play playerID move game = lift . runWriterT . runExceptT $ play key move game
if playing game == playerID | Hanafuda.KoiKoi.playing game == key =
then KoiKoi.play move game Hanafuda.KoiKoi.play move game
else throwError "Not your turn" | otherwise = throwError "Not your turn"

27
src/JSON.hs Normal file
View file

@ -0,0 +1,27 @@
module JSON (
defaultOptions
, distinct
, singleLCField
) where
import Data.Char (toLower)
import Data.Aeson (
Options(..)
, SumEncoding(..)
, defaultOptions
)
first :: (a -> a) -> [a] -> [a]
first _ [] = []
first f (x:xs) = f x:xs
singleLCField :: Options
singleLCField = defaultOptions {
constructorTagModifier = (toLower `first`)
, sumEncoding = ObjectWithSingleField
}
distinct :: Options
distinct = defaultOptions {
sumEncoding = UntaggedValue
}

View file

@ -2,34 +2,33 @@
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
module Main where module Main where
import qualified App (Context(..), T, update_) import Network.Wai.Handler.Warp (run)
import qualified Automaton (start) import Network.HTTP.Types.Status (badRequest400)
import qualified Config (listenPort) import Network.WebSockets (ServerApp, acceptRequest, defaultConnectionOptions)
import Network.Wai.Handler.WebSockets (websocketsOr)
import Network.Wai (responseLBS)
import Control.Monad.Reader (ReaderT(..), asks)
import Control.Concurrent (newMVar, modifyMVar) import Control.Concurrent (newMVar, modifyMVar)
import Control.Exception (finally) import Control.Exception (finally)
import Control.Monad.Reader (ReaderT(..), asks) import qualified Config (listenPort)
import qualified Hanafuda.Message as Message (FromClient(..))
import Messaging (broadcast, relay)
import Network.HTTP.Types.Status (badRequest400)
import Network.Wai (responseLBS)
import Network.Wai.Handler.Warp (run)
import Network.Wai.Handler.WebSockets (websocketsOr)
import Network.WebSockets (ServerApp, acceptRequest, defaultConnectionOptions)
import qualified Server (disconnect, new, register)
import qualified Session (open) import qualified Session (open)
import qualified Server (disconnect, new, register)
import qualified App (Context(..), T, update_)
import qualified Message (FromClient(..), broadcast, relay)
import qualified Automaton (start)
exit :: App.T () exit :: App.T ()
exit = do exit = do
asks App.playerID >>= App.update_ . Server.disconnect asks App.key >>= App.update_ . Server.disconnect
relay Message.LogOut broadcast Message.relay Message.LogOut Message.broadcast
serverApp :: App.T () -> App.T () -> IO ServerApp serverApp :: App.T () -> App.T () -> IO ServerApp
serverApp onEnter onExit = do serverApp onEnter onExit = do
mServer <- newMVar Server.new mServer <- newMVar Server.new
return $ \pending -> do return $ \pending -> do
session <- Session.open <$> acceptRequest pending session <- Session.open <$> acceptRequest pending
playerID <- modifyMVar mServer (return . Server.register session) key <- modifyMVar mServer (return . Server.register session)
let app = App.Context {App.mServer, App.playerID} let app = App.Context {App.mServer, App.key}
finally finally
(runReaderT onEnter app) (runReaderT onEnter app)
(runReaderT onExit app) (runReaderT onExit app)

104
src/Message.hs Normal file
View file

@ -0,0 +1,104 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
module Message (
FromClient(..)
, T(..)
, broadcast
, get
, notifyPlayers
, receive
, relay
, send
, sendTo
, update
) where
import Data.List (intercalate)
import Data.Foldable (forM_)
import Data.Map (keys)
import Data.Aeson (FromJSON(..), ToJSON(..), Value, eitherDecode', encode, genericParseJSON, genericToEncoding, defaultOptions)
import Network.WebSockets (receiveData, sendTextData)
import Data.ByteString.Lazy.Char8 (unpack)
import Data.Text (Text)
import Control.Monad.Reader (asks, lift)
import qualified Player (Key)
import qualified Game (T, export)
import qualified Session (T(..))
import qualified Server (T(..), get)
import qualified App (Context(..), T, connection, debug, server)
import qualified Hanafuda.KoiKoi as KoiKoi (Action, Game(..), Move(..))
import GHC.Generics (Generic)
data FromClient =
Answer {accept :: Bool}
| Invitation {to :: Player.Key}
| LogIn {name :: Text}
| LogOut
| Play {move :: KoiKoi.Move}
| Ping
deriving (Generic)
instance ToJSON FromClient where
toEncoding = genericToEncoding defaultOptions
instance FromJSON FromClient where
parseJSON = genericParseJSON defaultOptions
data T =
Relay {from :: Player.Key, message :: FromClient}
| Welcome {room :: Server.T, key :: Player.Key}
| Update {alone :: [Player.Key], paired :: [Player.Key]}
| Game {game :: Value, logs :: [KoiKoi.Action]}
| Pong
| Error {error :: String}
deriving (Generic)
instance ToJSON T where
toEncoding = genericToEncoding defaultOptions
sendTo :: [Player.Key] -> T -> App.T ()
sendTo playerKeys obj = do
sessions <- getSessions <$> App.server
App.debug $ '(' : intercalate ", " recipients ++ ") <" ++ (unpack encoded)
lift $ forM_ (Session.connection <$> sessions) $ flip sendTextData encoded
where
encoded = encode $ obj
getSessions server = (\key -> Server.get key server) <$> playerKeys
recipients = show <$> playerKeys
send :: T -> App.T ()
send obj = do
key <- asks App.key
sendTo [key] obj
broadcast :: T -> App.T ()
broadcast obj =
App.server >>= flip sendTo obj . keys . Server.sessions
relay :: FromClient -> (T -> App.T ()) -> App.T ()
relay message f = do
App.debug "Relaying"
(\from -> f $ Relay {from, message}) =<< asks App.key
receive :: App.T FromClient
receive = do
received <- ((lift . receiveData) =<< App.connection)
App.debug $ '>':(unpack received)
case eitherDecode' received of
Left errorMessage -> send (Message.Error errorMessage) >> receive
Right clientMessage -> return clientMessage
get :: App.T Message.FromClient
get =
receive >>= pong
where
pong Ping = send Pong >> get
pong m = return m
update :: T
update = Update {alone = [], paired = []}
notifyPlayers :: Game.T -> [KoiKoi.Action] -> App.T ()
notifyPlayers game logs =
forM_ (keys $ KoiKoi.scores game) $ \k ->
sendTo [k] $ Game {game = Game.export k game, logs}

View file

@ -1,76 +0,0 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE FlexibleContexts #-}
module Messaging (
FromClient(..)
, T(..)
, broadcast
, get
, notifyPlayers
, receive
, relay
, send
, sendTo
, update
) where
import qualified App (Context(..), T, connection, debug, server)
import Control.Monad.Reader (asks, lift)
import Data.Aeson (eitherDecode', encode)
import Data.ByteString.Lazy.Char8 (unpack)
import Data.Foldable (forM_)
import Data.List (intercalate)
import Data.Map (keys)
import qualified Hanafuda.KoiKoi as KoiKoi (Action, Game, GameBlueprint(..), PlayerID)
import Hanafuda.Message (FromClient(..), T(..))
import qualified Hanafuda.Message as Message (T)
import Network.WebSockets (receiveData, sendTextData)
import qualified Game (export)
import qualified Server (T(..), get)
import qualified Session (T(..))
sendTo :: [KoiKoi.PlayerID] -> Message.T -> App.T ()
sendTo playerIDs obj = do
sessions <- getSessions <$> App.server
App.debug $ '(' : intercalate ", " recipients ++ ") <" ++ (unpack encoded)
lift $ forM_ (Session.connection <$> sessions) $ flip sendTextData encoded
where
encoded = encode $ obj
getSessions server = (\playerID -> Server.get playerID server) <$> playerIDs
recipients = show <$> playerIDs
send :: Message.T -> App.T ()
send obj = do
playerID <- asks App.playerID
sendTo [playerID] obj
broadcast :: Message.T -> App.T ()
broadcast obj =
App.server >>= flip sendTo obj . keys . Server.sessions
relay :: FromClient -> (Message.T -> App.T ()) -> App.T ()
relay message f = do
App.debug "Relaying"
(\from -> f $ Relay {from, message}) =<< asks App.playerID
receive :: App.T FromClient
receive = do
received <- ((lift . receiveData) =<< App.connection)
App.debug $ '>':(unpack received)
case eitherDecode' received of
Left errorMessage -> send (Error errorMessage) >> receive
Right clientMessage -> return clientMessage
get :: App.T FromClient
get =
receive >>= pong
where
pong Ping = send Pong >> get
pong m = return m
update :: T
update = Update {alone = [], paired = []}
notifyPlayers :: KoiKoi.Game -> [KoiKoi.Action] -> App.T ()
notifyPlayers game logs =
forM_ (keys $ KoiKoi.scores game) $ \k ->
sendTo [k] $ Game {game = Game.export k game, logs}

30
src/Player.hs Normal file
View file

@ -0,0 +1,30 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Player (
Key
, T(..)
) where
import Data.Text (Text)
import qualified Data (Key)
import GHC.Generics
data T = T {
name :: Text
} deriving (Eq, Ord, Generic)
type Key = Data.Key T
{-
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
-}

View file

@ -1,11 +0,0 @@
{-# LANGUAGE MultiParamTypeClasses #-}
module RW (
RW(..)
) where
class RW a b where
get :: b -> a
set :: a -> b -> b
update :: (a -> a) -> b -> b
update f v =
set (f (get v)) v

View file

@ -2,7 +2,9 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
module Server ( module Server (
T(..) T(..)
, disconnect , disconnect
@ -12,24 +14,25 @@ module Server (
, logOut , logOut
, new , new
, register , register
, room
, update , update
) where ) where
import Data.Aeson (ToJSON(..), (.=), object, pairs)
import Data.Map (Map, (!), (!?), adjust, delete, insert, lookupMax, mapWithKey) import Data.Map (Map, (!), (!?), adjust, delete, insert, lookupMax, mapWithKey)
import qualified Data.Map as Map (empty) import qualified Data.Map as Map (empty)
import Data.Monoid ((<>))
import Data.Set (Set, member) import Data.Set (Set, member)
import qualified Data.Set as Set (delete, empty, insert) import qualified Data.Set as Set (delete, empty, insert)
import Data.Text (Text) import Data.Text (Text)
import Hanafuda.KoiKoi (Game, GameID, PlayerID) import qualified Data (RW(..))
import Hanafuda.Message (PlayerStatus(..), Room) import qualified Game (Key, T)
import qualified RW (RW(..)) import qualified Player (Key, T(..))
import qualified Session (Status(..), T(..), Update) import qualified Session (Status(..), T(..), Update)
type Names = Set Text type Names = Set Text
type Players = Map PlayerID Text type Players = Map Player.Key Player.T
type Sessions = Map PlayerID Session.T type Sessions = Map Player.Key Session.T
type Games = Map GameID Game type Games = Map Game.Key Game.T
data T = T { data T = T {
names :: Names names :: Names
, players :: Players , players :: Players
@ -37,32 +40,38 @@ data T = T {
, games :: Games , games :: Games
} }
instance RW.RW Names T where instance Data.RW Names T where
get = names get = names
set names server = server {names} set names server = server {names}
instance RW.RW Players T where instance Data.RW Players T where
get = players get = players
set players server = server {players} set players server = server {players}
instance RW.RW Sessions T where instance Data.RW Sessions T where
get = sessions get = sessions
set sessions server = server {sessions} set sessions server = server {sessions}
instance RW.RW Games T where instance Data.RW Games T where
get = games get = games
set games server = server {games} set games server = server {games}
export :: Sessions -> PlayerID -> Text -> PlayerStatus newtype Player = Player (Text, Bool)
export sessions playerID playerName = PlayerStatus (playerName, alone) instance ToJSON Player where
toJSON (Player (name, alone)) = object ["name" .= name, "alone" .= alone]
toEncoding (Player (name, alone)) = pairs ("name" .= name <> "alone" .= alone)
export :: Sessions -> Player.Key -> Player.T -> Player
export sessions key player = Player (Player.name player, alone)
where where
alone = alone =
case Session.status (sessions ! playerID) of case Session.status (sessions ! key) of
Session.LoggedIn True -> True Session.LoggedIn True -> True
_ -> False _ -> False
room :: T -> Room instance ToJSON T where
room (T {players, sessions}) = mapWithKey (export sessions) players toJSON (T {players, sessions}) = toJSON $ mapWithKey (export sessions) players
toEncoding (T {players, sessions}) = toEncoding $ mapWithKey (export sessions) players
new :: T new :: T
new = T { new = T {
@ -72,41 +81,41 @@ new = T {
, games = Map.empty , games = Map.empty
} }
register :: forall a b. (Enum a, Ord a, RW.RW (Map a b) T) => b -> T -> (T, a) register :: forall a b. (Enum a, Ord a, Data.RW (Map a b) T) => b -> T -> (T, a)
register x server = register x server =
let playerID = maybe (toEnum 0) (\(n, _) -> succ n) $ lookupMax $ (RW.get server :: Map a b) in let key = maybe (toEnum 0) (\(n, _) -> succ n) $ lookupMax $ (Data.get server :: Map a b) in
(RW.update (insert playerID x) server, playerID) (Data.update (insert key x) server, key)
get :: forall a b. (Ord a, RW.RW (Map a b) T) => a -> T -> b get :: forall a b. (Ord a, Data.RW (Map a b) T) => a -> T -> b
get playerID server = (RW.get server :: Map a b) ! playerID get key server = (Data.get server :: Map a b) ! key
update :: forall a b. (Ord a, RW.RW (Map a b) T) => a -> (b -> b) -> T -> T update :: forall a b. (Ord a, Data.RW (Map a b) T) => a -> (b -> b) -> T -> T
update playerID updator = update key updator =
RW.update (adjust updator playerID :: Map a b -> Map a b) Data.update (adjust updator key :: Map a b -> Map a b)
disconnect :: PlayerID -> T -> T disconnect :: Player.Key -> T -> T
disconnect playerID = disconnect key =
RW.update (delete playerID :: Sessions -> Sessions) . logOut playerID Data.update (delete key :: Sessions -> Sessions) . logOut key
endGame :: GameID -> T -> T endGame :: Game.Key -> T -> T
endGame playerID = endGame key =
RW.update (delete playerID :: Games -> Games) Data.update (delete key :: Games -> Games)
logIn :: Text -> PlayerID -> T -> Either String T logIn :: Text -> Player.Key -> T -> Either String T
logIn name playerID server = logIn name key server =
RW.update (Set.insert name) . Data.update (Set.insert name) .
RW.update (insert playerID name) . Data.update (insert key $ Player.T {Player.name}) .
update playerID (RW.set $ Session.LoggedIn True :: Session.Update) <$> update key (Data.set $ Session.LoggedIn True :: Session.Update) <$>
if name `member` names server if name `member` names server
then Left "This name is already registered" then Left "This name is already registered"
else Right server else Right server
logOut :: PlayerID -> T -> T logOut :: Player.Key -> T -> T
logOut playerID server = logOut key server =
maybe maybe
server server
(\playerName -> (\player ->
RW.update (delete playerID :: Players -> Players) $ Data.update (delete key :: Players -> Players) $
update playerID (RW.set $ Session.LoggedIn False :: Session.Update) $ update key (Data.set $ Session.LoggedIn False :: Session.Update) $
RW.update (Set.delete playerName :: Names -> Names) server) Data.update (Set.delete $ Player.name player :: Names -> Names) server)
(players server !? playerID) (players server !? key)

View file

@ -1,5 +1,6 @@
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DeriveGeneric #-}
module Session ( module Session (
Status(..) Status(..)
, T(..) , T(..)
@ -7,16 +8,23 @@ module Session (
, open , open
) where ) where
import Hanafuda.KoiKoi (GameID, PlayerID)
import Network.WebSockets (Connection) import Network.WebSockets (Connection)
import qualified RW (RW(..)) import Data.Aeson (ToJSON(..), genericToEncoding)
import GHC.Generics (Generic)
import qualified JSON (singleLCField)
import qualified Data (RW(..))
import qualified Player (Key)
import qualified Game (Key)
data Status = data Status =
LoggedIn Bool LoggedIn Bool
| Answering PlayerID | Answering Player.Key
| Waiting PlayerID | Waiting Player.Key
| Playing GameID | Playing Game.Key
deriving (Show) deriving (Show, Generic)
instance ToJSON Status where
toEncoding = genericToEncoding JSON.singleLCField
data T = T { data T = T {
connection :: Connection connection :: Connection
@ -24,7 +32,7 @@ data T = T {
} }
type Update = T -> T type Update = T -> T
instance RW.RW Status T where instance Data.RW Status T where
get = status get = status
set status session = session {status} set status session = session {status}

View file

@ -9,9 +9,17 @@ function Game(modules) {
}; };
var sets = buildSets(); var sets = buildSets();
var selected = null; var selected = null;
var turnedCard = null;
var queue = []; var queue = [];
window.addEventListener('focus', runQueue);
modules.messaging.addEventListener(["Game"], function(o) {
if(document.hasFocus()) {
modules.async.run(handleGameMessage(o));
} else {
queue.push(handleGameMessage(o));
}
});
function buildSets() { function buildSets() {
var sets = {}; var sets = {};
['river', 'you', 'them'].forEach(function(id) { ['river', 'you', 'them'].forEach(function(id) {
@ -30,35 +38,17 @@ function Game(modules) {
return sets; return sets;
} }
window.addEventListener('focus', runQueue);
modules.messaging.addEventListener(["Game"], function(o) {
queue.push(handleGameMessage(o));
if(document.hasFocus() && queue.length == 1) {
runQueue();
} else {
modules.statusHandler.set("♪");
}
});
function runQueue() { function runQueue() {
if(queue.length > 0) { if(queue.length > 0) {
var length = queue.length;
modules.async.run.apply(null, queue.concat( modules.async.run.apply(null, queue.concat(
modules.async.apply(function() { modules.async.apply(function() {queue = [];})
queue = queue.slice(length);
runQueue();
})
)); ));
} }
} }
function handleGameMessage(o) { function handleGameMessage(o) {
if(o.game.deck == 24) { // deck is full, means new round if(o.game.deck == 24) {
if(o.logs.length > 0) { // but still some logs, from the previous round return o.logs.length > 0 ? modules.async.sequence(applyDiff(o), setGame(o)) : setGame(o);
return modules.async.sequence(applyDiff(o), setGame(o)); // so play the diff, then set the new round
} else {
return setGame(o); // directly set a whole new game
}
} else { } else {
return applyDiff(o); return applyDiff(o);
} }
@ -79,75 +69,39 @@ function Game(modules) {
function handleStep(o) { function handleStep(o) {
return function(f) { return function(f) {
handleTurnedCard(o, f); if(status.step == "Turned") {
if(status.step == "Scored") { setTurned(o.game.step.contents);
if(status.playing) { } else {
askKoikoi(o, f); if(status.step == "ToPlay" && o.game.playing == o.game.oyake) {
} else { rest.className = ["card", "count" + o.game.deck].join(' ');
theyScored(o, f); }
} if(deck.lastChild.id != "rest") {
} else if (status.step == "Over") { deck.removeChild(deck.lastChild);
gameEnd(o, f); }
} else { }
if(status.step == "Scored") {
if(status.playing) {
modules.screen.dialog({
text: modules.i18n.get('youScored'),
answers: [
{label: 'endRound', action: function() {play({koiKoi: false}); f();}},
{label: 'koikoi', action: function() {play({koiKoi: true}); f();}}
]
});
} else {
modules.screen.dialog({
text: modules.i18n.get('theyScored')(modules.room.name(o.game.playing)),
answers: [
{label: 'ok', action: f}
]
});
}
} else {
f(); f();
} }
}; };
} }
function handleTurnedCard(o, f) {
if(status.step == "Turned") {
setTurned(o.game.step.contents);
} else {
if(status.step == "ToPlay" && o.game.playing == o.game.oyake) {
rest.className = ["card", "count" + o.game.deck].join(' ');
}
if(deck.lastChild.id != "rest") {
deck.removeChild(deck.lastChild);
}
}
}
function askKoikoi(o, f) {
modules.screen.dialog({
text: modules.i18n.get('youScored'),
answers: [
{label: 'endRound', action: function() {play({koiKoi: false}); f();}},
{label: 'koikoi', action: function() {play({koiKoi: true}); f();}}
]
});
}
function theyScored(o, f) {
modules.screen.dialog({
text: modules.i18n.get('theyScored')(modules.room.name(o.game.playing)),
answers: [
{label: 'ok', action: f}
]
});
}
function gameEnd(o, f) {
var winner, maxScore;
for(var key in o.game.scores) {
if(maxScore == undefined || o.game.scores[key] > maxScore) {
winner = key;
maxScore = o.game.scores[key];
}
}
modules.screen.dialog({
text: modules.i18n.get(modules.session.is(winner) ? 'won' : 'lost'),
answers: [{
label: 'endGame',
action: function() {
modules.messaging.send({tag: "Quit"});
modules.screen.select('reception');
f();
}
}]
});
}
function applyDiff(o) { function applyDiff(o) {
return modules.async.sequence.apply(null, return modules.async.sequence.apply(null,
o.logs.map(animate).concat( o.logs.map(animate).concat(
@ -175,8 +129,7 @@ function Game(modules) {
movingCards.push([sets[side].hand, dest, card]); movingCards.push([sets[side].hand, dest, card]);
} else { } else {
var cardSet = {}; var cardSet = {};
cardSet[card.name] = turnedCard || new TurnedCard(card.name); cardSet[card.name] = turnedCard(card.name);
turnedCard = null;
movingCards.push([{card: cardSet, dom: deck}, dest, card]); movingCards.push([{card: cardSet, dom: deck}, dest, card]);
} }
return movingCards; return movingCards;
@ -191,7 +144,7 @@ function Game(modules) {
function moveCard(fromSet, toSet, card) { function moveCard(fromSet, toSet, card) {
var from, originalCard; var from, originalCard;
var slot = modules.dom.make('li', {class: ['card', 'slot']}); var slot = modules.dom.make('li', {class: ['card', 'slot']});
if (fromSet.card[card.name] != undefined) { if (fromSet.card[card.name] != undefined) {
originalCard = fromSet.card[card.name].dom; originalCard = fromSet.card[card.name].dom;
delete fromSet.card[card.name]; delete fromSet.card[card.name];
@ -302,11 +255,18 @@ function Game(modules) {
} }
} }
function turnedCard(cardName) {
var card = new Card(cardName);
card.dom.id = "turned";
deck.appendChild(card.dom);
return card;
}
function setTurned(cardName) { function setTurned(cardName) {
turnedCard = new TurnedCard(cardName); turnedCard(cardName);
if(status.playing) { if(status.playing) {
selected = turnedCard; selected = cardName;
showCandidates(modules.hanafuda.Card[cardName], true); showCandidates(modules.hanafuda.Card[selected], true);
} }
} }
@ -314,12 +274,6 @@ function Game(modules) {
matchingInRiver(card).forEach(function(riverCard) {riverCard.setCandidate(yes);}); matchingInRiver(card).forEach(function(riverCard) {riverCard.setCandidate(yes);});
} }
function setSelected(yes) {
selected = yes ? this : null;
this.dom.classList.toggle('selected', yes);
showCandidates(this.value, yes);
}
function Card(name) { function Card(name) {
this.value = modules.hanafuda.Card[name]; this.value = modules.hanafuda.Card[name];
this.name = name; this.name = name;
@ -344,8 +298,9 @@ function Game(modules) {
var card = this; var card = this;
return function() { return function() {
if(card.candidate) { if(card.candidate) {
var withCard = selected.name; var withCard = selected;
selected.setSelected(false); selected = null;
showCandidates(card.value, false);
play( play(
status.step == 'ToPlay' ? {capture: [withCard, card.name]} : {choose: card.name} status.step == 'ToPlay' ? {capture: [withCard, card.name]} : {choose: card.name}
); );
@ -358,15 +313,6 @@ function Game(modules) {
this.dom.classList.toggle("candidate", yes); this.dom.classList.toggle("candidate", yes);
} }
function TurnedCard() {
Card.apply(this, arguments);
this.dom.id = "turned";
deck.appendChild(this.dom);
}
TurnedCard.prototype.onClick = Card.prototype.onClick;
TurnedCard.prototype.setSelected = setSelected;
function HandCard() { function HandCard() {
Card.apply(this, arguments); Card.apply(this, arguments);
} }
@ -376,7 +322,7 @@ function Game(modules) {
return function() { return function() {
if(status.playing && status.step == "ToPlay") { if(status.playing && status.step == "ToPlay") {
if(selected != undefined) { if(selected != undefined) {
selected.setSelected(false); sets.you.hand.card[selected].setSelected(false);
} else { } else {
card.play(); card.play();
} }
@ -384,7 +330,11 @@ function Game(modules) {
}; };
}; };
HandCard.prototype.setSelected = setSelected; HandCard.prototype.setSelected = function(yes) {
selected = yes ? this.name : null;
this.dom.classList.toggle('selected', yes);
showCandidates(this.value, yes);
}
HandCard.prototype.play = function() { HandCard.prototype.play = function() {
var matching = matchingInRiver(this.value); var matching = matchingInRiver(this.value);

View file

@ -12,7 +12,6 @@
<script src="messaging.js"></script> <script src="messaging.js"></script>
<script src="session.js"></script> <script src="session.js"></script>
<script src="room.js"></script> <script src="room.js"></script>
<script src="statusHandler.js"></script>
<script src="login.js"></script> <script src="login.js"></script>
<script src="hanafuda.js"></script> <script src="hanafuda.js"></script>
<script src="game.js"></script> <script src="game.js"></script>
@ -70,6 +69,6 @@
</div> </div>
<div id="dialog"> <div id="dialog">
</div> </div>
<p id="error"></p> <p id="debug"></p>
</body> </body>
</html> </html>

View file

@ -56,7 +56,6 @@ function Login(modules) {
var name = modules.room.name(o.from); var name = modules.room.name(o.from);
// invitations should come only from known players, in doubt say «no» // invitations should come only from known players, in doubt say «no»
if(name) { if(name) {
modules.statusHandler.set("🎴");
modules.screen.dialog({ modules.screen.dialog({
text: modules.i18n.get('invited')(name), text: modules.i18n.get('invited')(name),
answers: invitationAnswers answers: invitationAnswers

View file

@ -5,13 +5,12 @@ window.addEventListener('load', function() {
var i18n = I18n({translations: translations}); var i18n = I18n({translations: translations});
var fun = Fun(); var fun = Fun();
var screen = Screen({dom: dom, i18n: i18n}); var screen = Screen({dom: dom, i18n: i18n});
var messaging = Messaging({screen: screen}); var messaging = Messaging();
var session = Session({messaging: messaging}); var session = Session({messaging: messaging});
var room = Room({dom: dom, messaging: messaging, session: session, fun: fun}); var room = Room({dom: dom, messaging: messaging, session: session, fun: fun});
var statusHandler = StatusHandler(); var login = Login({dom: dom, i18n: i18n, messaging: messaging, room: room, screen: screen, session: session});
var login = Login({dom: dom, i18n: i18n, messaging: messaging, room: room, screen: screen, session: session, statusHandler: statusHandler});
var hanafuda = Hanafuda({fun: fun}); var hanafuda = Hanafuda({fun: fun});
var game = Game({async: async, dom: dom, i18n: i18n, fun: fun, hanafuda: hanafuda, messaging: messaging, room: room, screen: screen, session: session, statusHandler: statusHandler}); var game = Game({async: async, dom: dom, i18n: i18n, fun: fun, hanafuda: hanafuda, messaging: messaging, room: room, screen: screen, session: session});
var domElems = { var domElems = {
join: document.getElementById('login').join, join: document.getElementById('login').join,

View file

@ -1,7 +1,5 @@
function Messaging(modules) { function Messaging(screen) {
var ws = new WebSocket(window.location.origin.replace(/^http/, 'ws') + '/play/'); var ws = new WebSocket('ws://' + window.location.hostname + '/play/');
var debug = getParameters().debug;
var doLog = debug != undefined && debug.match(/^(?:1|t(?:rue)?|v(?:rai)?)$/i);
var keepAlivePeriod = 20000; var keepAlivePeriod = 20000;
var routes = {callbacks: [], children: {}}; var routes = {callbacks: [], children: {}};
@ -27,15 +25,6 @@ function Messaging(modules) {
} }
} }
function getParameters() {
var o = {};
window.location.search.substr(1).split('&').forEach(function(s) {
var t = s.split('=');
o[t[0]] = t[1];
});
return o;
}
function addEventListener(path, callback) { function addEventListener(path, callback) {
var route = get(routes, path, true); var route = get(routes, path, true);
route.callbacks.push(callback); route.callbacks.push(callback);
@ -53,29 +42,18 @@ function Messaging(modules) {
if(route != undefined && route.callbacks != undefined) { if(route != undefined && route.callbacks != undefined) {
route.callbacks.forEach(function(f) {f(o);}); route.callbacks.forEach(function(f) {f(o);});
} else { } else {
console.log("No route found for " + event.data); debug.textContent = event.data;
} }
o.direction = 'client < server';
log(o);
}; };
function log(message) {
if(doLog) {
console.log(message);
}
}
function start() { function start() {
ws.addEventListener('message', messageListener); ping();
ws.addEventListener('open', ping);
addEventListener(["Pong"], ping); addEventListener(["Pong"], ping);
addEventListener(["Error"], function(o) {modules.screen.error(o.error);}); ws.addEventListener('message', messageListener);
} }
function send(o) { function send(o) {
ws.send(JSON.stringify(o)); ws.send(JSON.stringify(o));
o.direction = 'client > server';
log(o);
} }
function ping() { function ping() {

View file

@ -33,22 +33,3 @@ body > div.on {
#dialog button { #dialog button {
display: inline-block; display: inline-block;
} }
#error {
position: absolute;
z-index: 1;
top: 1em;
right: 1em;
max-width: 20em;
border: 1px solid #e0afac;
padding: 1em;
border-radius: 0.5em;
background: bisque;
cursor: pointer;
margin: 0;
display: none;
}
#error.on {
display: block;
}

View file

@ -1,12 +1,7 @@
function Screen(modules) { function Screen(modules) {
var current = document.querySelector("body > div.on"); var current = document.querySelector("body > div.on");
var errorBox = document.getElementById('error');
errorBox.addEventListener('click', function() {
errorBox.className = "";
});
return { return {
error: error,
dialog: dialog, dialog: dialog,
select: select select: select
}; };
@ -40,9 +35,4 @@ function Screen(modules) {
layer.appendChild(dialog); layer.appendChild(dialog);
layer.className = "on"; layer.className = "on";
} }
function error(message) {
errorBox.textContent = message;
errorBox.className = "on";
}
} }

View file

@ -1,20 +0,0 @@
function StatusHandler() {
var baseTitle = document.title;
window.addEventListener('focus', reset);
return {
reset: reset,
set: set
};
function reset() {
document.title = baseTitle;
}
function set(newStatus) {
if(!document.hasFocus()) {
document.title = newStatus + " - " + baseTitle;
}
}
}

View file

@ -17,7 +17,6 @@ function Translations() {
alone: "No one to play with yet ! Wait a little", alone: "No one to play with yet ! Wait a little",
decline: "Decline", decline: "Decline",
endRound: "End the round", endRound: "End the round",
endGame: "Return to main menu",
join: "Join", join: "Join",
invite: "Invite", invite: "Invite",
invited: function(name) { invited: function(name) {
@ -25,7 +24,6 @@ function Translations() {
}, },
koikoi: "KoiKoi !!", koikoi: "KoiKoi !!",
leave: "Leave", leave: "Leave",
lost: "You lost the game",
monthFlower: function(flower) { monthFlower: function(flower) {
return "This month's flower is the " + flower; return "This month's flower is the " + flower;
}, },
@ -39,7 +37,6 @@ function Translations() {
theyScored: function(name) { theyScored: function(name) {
return name + " scored"; return name + " scored";
}, },
won: "You won !",
yourTurn: "Your turn", yourTurn: "Your turn",
youScored: "You scored ! Do you want to get your points and end the round or KoiKoi ?" youScored: "You scored ! Do you want to get your points and end the round or KoiKoi ?"
}, },
@ -60,7 +57,6 @@ function Translations() {
alone: "Personne pour jouer pour l'instant ! Attendez un peu", alone: "Personne pour jouer pour l'instant ! Attendez un peu",
decline: "Refuser", decline: "Refuser",
endRound: "Finir la manche", endRound: "Finir la manche",
endGame: "Retourner au menu principal",
join: "Entrer", join: "Entrer",
invite: "Inviter", invite: "Inviter",
invited: function(name) { invited: function(name) {
@ -68,7 +64,6 @@ function Translations() {
}, },
koikoi: "KoiKoi !!", koikoi: "KoiKoi !!",
leave: "Partir", leave: "Partir",
lost: "Vous avez perdu",
monthFlower: function(flower) { monthFlower: function(flower) {
return "C'est le mois des " + flower; return "C'est le mois des " + flower;
}, },
@ -82,7 +77,6 @@ function Translations() {
theyScored: function(name) { theyScored: function(name) {
return name + " a marqué"; return name + " a marqué";
}, },
won: "Vous avez gagné !",
yourTurn: "À vous", yourTurn: "À vous",
youScored: "Vous avez marqué ! Voulez-vous empocher vos gains et terminer la manche ou faire KoiKoi ?" youScored: "Vous avez marqué ! Voulez-vous empocher vos gains et terminer la manche ou faire KoiKoi ?"
} }