Compare commits
11 commits
handle-gam
...
main
Author | SHA1 | Date | |
---|---|---|---|
c024ea261c | |||
b03c7fd087 | |||
6dfaaee385 | |||
0abc020d13 | |||
c21a8f6512 | |||
0778c4a675 | |||
e9205b67c7 | |||
0f685c0a98 | |||
ac114c16df | |||
b2253b81d6 | |||
66d2926635 |
24 changed files with 473 additions and 514 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
/dist/*
|
/dist*
|
||||||
|
cabal.project.local
|
||||||
|
.ghc*
|
||||||
|
|
13
ChangeLog.md
13
ChangeLog.md
|
@ -1,5 +1,18 @@
|
||||||
# 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
|
||||||
|
|
|
@ -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.1.0
|
version: 0.2.3.0
|
||||||
synopsis: A webapp for the Haskell hanafuda library
|
synopsis: A webapp for the Haskell hanafuda library
|
||||||
-- description:
|
-- description:
|
||||||
homepage: https://framagit.org/hanafuda
|
homepage: https://git.marvid.fr/hanafuda
|
||||||
license: BSD3
|
license: BSD3
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Sasha
|
author: Tissevert
|
||||||
maintainer: sasha+frama@marvid.fr
|
maintainer: tissevert+devel@marvid.fr
|
||||||
-- copyright:
|
-- copyright:
|
||||||
category: Web
|
category: Web
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
@ -17,26 +17,25 @@ 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://framagit.org/hanafuda/api
|
location: https://git.marvid.fr/hanafuda/webapp
|
||||||
|
|
||||||
executable hanafudapi
|
executable hanafudapi
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
other-modules: App
|
other-modules: App
|
||||||
, Automaton
|
, Automaton
|
||||||
, Config
|
, Config
|
||||||
, Message
|
, Messaging
|
||||||
, Game
|
, Game
|
||||||
, JSON
|
, RW
|
||||||
, Data
|
|
||||||
, Player
|
|
||||||
, Server
|
, Server
|
||||||
, Session
|
, Session
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
build-depends: base >=4.10 && <4.13
|
build-depends: base >=4.9 && <4.13
|
||||||
, bytestring
|
, bytestring
|
||||||
, containers
|
, containers >= 0.5.9
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
, hanafuda >= 0.3.0
|
, hanafuda >= 0.3.3
|
||||||
|
, hanafuda-APILanguage >= 0.1.0
|
||||||
, http-types
|
, http-types
|
||||||
, aeson
|
, aeson
|
||||||
, mtl
|
, mtl
|
||||||
|
|
18
src/App.hs
18
src/App.hs
|
@ -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 Player (Key)
|
|
||||||
import qualified Session (T(..))
|
|
||||||
import qualified Server (T(..))
|
import qualified Server (T(..))
|
||||||
|
import qualified Session (T(..))
|
||||||
|
|
||||||
data Context = Context {
|
data Context = Context {
|
||||||
mServer :: MVar Server.T
|
mServer :: MVar Server.T
|
||||||
, key :: Player.Key
|
, playerID :: PlayerID
|
||||||
}
|
}
|
||||||
|
|
||||||
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 :: Player.Key -> T Session.T
|
get :: PlayerID -> T Session.T
|
||||||
get key =
|
get playerID =
|
||||||
(! key) . Server.sessions <$> server
|
(! playerID) . Server.sessions <$> server
|
||||||
|
|
||||||
current :: T Session.T
|
current :: T Session.T
|
||||||
current = do
|
current = do
|
||||||
asks key >>= get
|
asks playerID >>= 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 key
|
show <$> asks playerID
|
||||||
>>= 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)
|
||||||
|
|
111
src/Automaton.hs
111
src/Automaton.hs
|
@ -3,95 +3,102 @@ module Automaton (
|
||||||
start
|
start
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad.Except (runExceptT)
|
import qualified App (Context(..), T, current, debug, get, server, try, update_)
|
||||||
import Control.Monad.Reader (asks, lift)
|
import Control.Monad.Reader (asks)
|
||||||
import Control.Monad.Writer (runWriterT)
|
import Data.Map (Map, (!?))
|
||||||
import qualified Data (RW(..))
|
|
||||||
import qualified Game (new, play)
|
import qualified Game (new, play)
|
||||||
import qualified Hanafuda.KoiKoi as KoiKoi (Game(..), players)
|
import qualified Hanafuda.KoiKoi as KoiKoi (
|
||||||
import qualified Hanafuda.Player as Player (next)
|
Game, GameBlueprint(..), GameID, Step(..)
|
||||||
|
)
|
||||||
|
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.key >>= App.try . (Server.logIn login)
|
asks App.playerID >>= App.try . (Server.logIn login)
|
||||||
>>= maybe
|
>>= maybe
|
||||||
(Message.relay logIn Message.broadcast >> move (Session.LoggedIn True))
|
(Messaging.relay logIn Messaging.broadcast >> setSessionStatus (Session.LoggedIn True))
|
||||||
sendError
|
sendError
|
||||||
|
|
||||||
receive (Session.LoggedIn True) logOut@Message.LogOut = do
|
receive (Session.LoggedIn True) logOut@Message.LogOut = do
|
||||||
Message.relay logOut Message.broadcast
|
Messaging.relay logOut Messaging.broadcast
|
||||||
asks App.key >>= App.update_ . Server.logOut
|
asks App.playerID >>= App.update_ . Server.logOut
|
||||||
move (Session.LoggedIn False)
|
setSessionStatus (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
|
||||||
key <- asks App.key
|
from <- asks App.playerID
|
||||||
App.update_ (Server.update to (Data.set $ Session.Answering key :: Session.Update))
|
App.update_ (Server.update to (RW.set $ Session.Answering from :: Session.Update))
|
||||||
Message.broadcast $ Message.update {Message.paired = [key, to]}
|
Messaging.broadcast $ Messaging.update {Message.paired = [from, to]}
|
||||||
(Message.relay invitation $ Message.sendTo [to])
|
(Messaging.relay invitation $ Messaging.sendTo [to])
|
||||||
move (Session.Waiting to)
|
setSessionStatus (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
|
||||||
key <- asks App.key
|
playerID <- asks App.playerID
|
||||||
case Session.status session of
|
case Session.status session of
|
||||||
Session.Waiting for | for == key -> do
|
Session.Waiting for | for == playerID -> do
|
||||||
Message.relay message $ Message.sendTo [to]
|
Messaging.relay message $ Messaging.sendTo [to]
|
||||||
newStatus <-
|
newStatus <-
|
||||||
if accept
|
if accept
|
||||||
then do
|
then do
|
||||||
gameKey <- Server.register <$> (lift $ Game.new for to) >>= App.update
|
gameID <- Game.new (for, to)
|
||||||
game <- Server.get gameKey <$> App.server
|
game <- Server.get gameID <$> App.server
|
||||||
Message.notifyPlayers game []
|
Messaging.notifyPlayers game []
|
||||||
return $ Session.Playing gameKey
|
return $ Session.Playing gameID
|
||||||
else do
|
else do
|
||||||
Message.broadcast $ Message.update {Message.alone = [key, to]}
|
Messaging.broadcast $ Messaging.update {Message.alone = [for, to]}
|
||||||
return $ Session.LoggedIn True
|
return $ Session.LoggedIn True
|
||||||
App.update_ $ Server.update to (Data.set newStatus :: Session.Update)
|
App.update_ $ Server.update to (RW.set newStatus :: Session.Update)
|
||||||
move newStatus
|
setSessionStatus newStatus
|
||||||
_ -> sendError "They're not waiting for your answer"
|
_ -> sendError "They're not waiting for your answer"
|
||||||
|
|
||||||
receive (Session.Playing gameKey) played@(Message.Play {}) = do
|
receive (Session.Playing gameID) played@(Message.Play {}) = do
|
||||||
key <- asks App.key
|
playerID <- asks App.playerID
|
||||||
game <- Server.get gameKey <$> App.server
|
game <- Server.get gameID <$> App.server
|
||||||
(result, logs) <- lift . runWriterT . runExceptT $ Game.play key (Message.move played) game
|
(result, logs) <- Game.play playerID (Message.move played) game
|
||||||
case result of
|
case result of
|
||||||
Left message -> sendError message
|
Left message -> sendError message
|
||||||
Right newGame ->
|
Right newGame -> do
|
||||||
if KoiKoi.on newGame
|
case KoiKoi.step newGame of
|
||||||
then do
|
KoiKoi.Over -> do
|
||||||
App.update_ $ Server.update gameKey (const newGame)
|
App.debug $ "Game " ++ show gameID ++ " ended"
|
||||||
Message.notifyPlayers newGame logs
|
App.update_ $ Server.endGame gameID
|
||||||
else do
|
_ -> App.update_ $ Server.update gameID (const newGame)
|
||||||
let newStatus = Session.LoggedIn True
|
Messaging.notifyPlayers newGame logs
|
||||||
let opponent = Player.next (KoiKoi.players newGame) key
|
|
||||||
App.update_ $ Server.endGame gameKey
|
receive (Session.Playing gameID) Message.Quit = do
|
||||||
App.update_ $ Server.update opponent (Data.set newStatus :: Session.Update)
|
games <- (RW.get <$> App.server :: App.T (Map KoiKoi.GameID KoiKoi.Game))
|
||||||
Message.notifyPlayers newGame logs
|
case games !? gameID of
|
||||||
move newStatus
|
Nothing -> do
|
||||||
|
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 = Message.send . Message.Error
|
sendError = Messaging.send . Message.Error
|
||||||
|
|
||||||
move :: Session.Status -> App.T ()
|
setSessionStatus :: Session.Status -> App.T ()
|
||||||
move newStatus = do
|
setSessionStatus newStatus = do
|
||||||
key <- asks App.key
|
playerID <- asks App.playerID
|
||||||
App.update_ $ Server.update key $ (Data.set newStatus :: Session.Update)
|
App.update_ $ Server.update playerID $ (RW.set newStatus :: Session.Update)
|
||||||
App.debug $ show newStatus
|
App.debug $ show newStatus
|
||||||
|
|
||||||
loop :: App.T ()
|
loop :: App.T ()
|
||||||
loop = do
|
loop = do
|
||||||
message <- Message.get
|
message <- Messaging.get
|
||||||
status <- Session.status <$> App.current
|
status <- Session.status <$> App.current
|
||||||
status `receive` message
|
status `receive` message
|
||||||
loop
|
loop
|
||||||
|
@ -99,5 +106,5 @@ loop = do
|
||||||
start :: App.T ()
|
start :: App.T ()
|
||||||
start = do
|
start = do
|
||||||
App.debug "Initial state"
|
App.debug "Initial state"
|
||||||
Message.Welcome <$> App.server <*> asks App.key >>= Message.send
|
Message.Welcome . Server.room <$> App.server <*> asks App.playerID >>= Messaging.send
|
||||||
loop
|
loop
|
||||||
|
|
30
src/Data.hs
30
src/Data.hs
|
@ -1,30 +0,0 @@
|
||||||
{-# 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)
|
|
124
src/Game.hs
124
src/Game.hs
|
@ -1,112 +1,40 @@
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
|
||||||
{-# LANGUAGE StandaloneDeriving #-}
|
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module Game (
|
module Game (
|
||||||
Key
|
export
|
||||||
, T
|
|
||||||
, export
|
|
||||||
, new
|
, new
|
||||||
, play
|
, play
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad.Except (throwError)
|
import qualified App (T, update)
|
||||||
import Data.Text (pack)
|
import Control.Monad.Except (runExceptT, throwError)
|
||||||
|
import Control.Monad.Reader (lift)
|
||||||
|
import Control.Monad.Writer (runWriterT)
|
||||||
import Data.Map (mapWithKey)
|
import Data.Map (mapWithKey)
|
||||||
import Data.HashMap.Strict (insert)
|
import qualified Hanafuda (empty)
|
||||||
import Data.Aeson (FromJSON(..), ToJSON(..), ToJSON1(..), ToJSONKey(..), Value(..), genericParseJSON, genericToEncoding, genericLiftToEncoding, toEncoding1, toJSON1)
|
import Hanafuda.KoiKoi (Game, GameBlueprint(..), GameID, Mode(..), PlayerID)
|
||||||
import Data.Aeson.Types (toJSONKeyText)
|
import qualified Hanafuda.KoiKoi as KoiKoi (
|
||||||
import qualified JSON (defaultOptions, singleLCField)
|
Action, Move(..), play, new
|
||||||
import qualified Data (Key)
|
)
|
||||||
import qualified Player (Key)
|
import Hanafuda.Message (PublicGame)
|
||||||
import qualified Hanafuda (Flower(..), Card(..), Pack, cardsOfPack, empty)
|
|
||||||
import qualified Hanafuda.Player (Player(..), Players(..))
|
import qualified Hanafuda.Player (Player(..), Players(..))
|
||||||
import qualified Hanafuda.KoiKoi (Action(..), Game(..), Environment, Mode(..), Move(..), Score, Source(..), Step(..), Yaku(..), new, play)
|
import qualified Server (register)
|
||||||
import GHC.Generics
|
|
||||||
|
|
||||||
deriving instance Generic Hanafuda.Card
|
new :: (PlayerID, PlayerID) -> App.T GameID
|
||||||
deriving instance Generic Hanafuda.Flower
|
new (for, to) =
|
||||||
deriving instance Generic Hanafuda.KoiKoi.Action
|
Server.register <$> (lift $ KoiKoi.new (for, to) WholeYear) >>= App.update
|
||||||
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)
|
|
||||||
|
|
||||||
type T = Hanafuda.KoiKoi.Game Player.Key
|
export :: PlayerID -> Game -> PublicGame
|
||||||
|
export playerID game = game {
|
||||||
deriving instance Generic T
|
deck = length $ deck game
|
||||||
|
, 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 == key = player
|
| k == playerID = 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 :: Hanafuda.KoiKoi.Environment m => Player.Key -> Hanafuda.KoiKoi.Move -> T -> m (Hanafuda.KoiKoi.Game Player.Key)
|
play :: PlayerID -> KoiKoi.Move -> Game -> App.T (Either String Game, [KoiKoi.Action])
|
||||||
play key move game
|
play playerID move game = lift . runWriterT . runExceptT $
|
||||||
| Hanafuda.KoiKoi.playing game == key =
|
if playing game == playerID
|
||||||
Hanafuda.KoiKoi.play move game
|
then KoiKoi.play move game
|
||||||
| otherwise = throwError "Not your turn"
|
else throwError "Not your turn"
|
||||||
|
|
27
src/JSON.hs
27
src/JSON.hs
|
@ -1,27 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
31
src/Main.hs
31
src/Main.hs
|
@ -2,33 +2,34 @@
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
import Network.Wai.Handler.Warp (run)
|
import qualified App (Context(..), T, update_)
|
||||||
import Network.HTTP.Types.Status (badRequest400)
|
import qualified Automaton (start)
|
||||||
import Network.WebSockets (ServerApp, acceptRequest, defaultConnectionOptions)
|
import qualified Config (listenPort)
|
||||||
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 qualified Config (listenPort)
|
import Control.Monad.Reader (ReaderT(..), asks)
|
||||||
import qualified Session (open)
|
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 Server (disconnect, new, register)
|
||||||
import qualified App (Context(..), T, update_)
|
import qualified Session (open)
|
||||||
import qualified Message (FromClient(..), broadcast, relay)
|
|
||||||
import qualified Automaton (start)
|
|
||||||
|
|
||||||
exit :: App.T ()
|
exit :: App.T ()
|
||||||
exit = do
|
exit = do
|
||||||
asks App.key >>= App.update_ . Server.disconnect
|
asks App.playerID >>= App.update_ . Server.disconnect
|
||||||
Message.relay Message.LogOut Message.broadcast
|
relay Message.LogOut 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
|
||||||
key <- modifyMVar mServer (return . Server.register session)
|
playerID <- modifyMVar mServer (return . Server.register session)
|
||||||
let app = App.Context {App.mServer, App.key}
|
let app = App.Context {App.mServer, App.playerID}
|
||||||
finally
|
finally
|
||||||
(runReaderT onEnter app)
|
(runReaderT onEnter app)
|
||||||
(runReaderT onExit app)
|
(runReaderT onExit app)
|
||||||
|
|
104
src/Message.hs
104
src/Message.hs
|
@ -1,104 +0,0 @@
|
||||||
{-# 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}
|
|
76
src/Messaging.hs
Normal file
76
src/Messaging.hs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{-# 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}
|
|
@ -1,30 +0,0 @@
|
||||||
{-# 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
|
|
||||||
-}
|
|
11
src/RW.hs
Normal file
11
src/RW.hs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{-# 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
|
|
@ -2,9 +2,7 @@
|
||||||
{-# 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
|
||||||
|
@ -14,25 +12,24 @@ 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 qualified Data (RW(..))
|
import Hanafuda.KoiKoi (Game, GameID, PlayerID)
|
||||||
import qualified Game (Key, T)
|
import Hanafuda.Message (PlayerStatus(..), Room)
|
||||||
import qualified Player (Key, T(..))
|
import qualified RW (RW(..))
|
||||||
import qualified Session (Status(..), T(..), Update)
|
import qualified Session (Status(..), T(..), Update)
|
||||||
|
|
||||||
type Names = Set Text
|
type Names = Set Text
|
||||||
type Players = Map Player.Key Player.T
|
type Players = Map PlayerID Text
|
||||||
type Sessions = Map Player.Key Session.T
|
type Sessions = Map PlayerID Session.T
|
||||||
type Games = Map Game.Key Game.T
|
type Games = Map GameID Game
|
||||||
data T = T {
|
data T = T {
|
||||||
names :: Names
|
names :: Names
|
||||||
, players :: Players
|
, players :: Players
|
||||||
|
@ -40,38 +37,32 @@ data T = T {
|
||||||
, games :: Games
|
, games :: Games
|
||||||
}
|
}
|
||||||
|
|
||||||
instance Data.RW Names T where
|
instance RW.RW Names T where
|
||||||
get = names
|
get = names
|
||||||
set names server = server {names}
|
set names server = server {names}
|
||||||
|
|
||||||
instance Data.RW Players T where
|
instance RW.RW Players T where
|
||||||
get = players
|
get = players
|
||||||
set players server = server {players}
|
set players server = server {players}
|
||||||
|
|
||||||
instance Data.RW Sessions T where
|
instance RW.RW Sessions T where
|
||||||
get = sessions
|
get = sessions
|
||||||
set sessions server = server {sessions}
|
set sessions server = server {sessions}
|
||||||
|
|
||||||
instance Data.RW Games T where
|
instance RW.RW Games T where
|
||||||
get = games
|
get = games
|
||||||
set games server = server {games}
|
set games server = server {games}
|
||||||
|
|
||||||
newtype Player = Player (Text, Bool)
|
export :: Sessions -> PlayerID -> Text -> PlayerStatus
|
||||||
instance ToJSON Player where
|
export sessions playerID playerName = PlayerStatus (playerName, alone)
|
||||||
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 ! key) of
|
case Session.status (sessions ! playerID) of
|
||||||
Session.LoggedIn True -> True
|
Session.LoggedIn True -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
instance ToJSON T where
|
room :: T -> Room
|
||||||
toJSON (T {players, sessions}) = toJSON $ mapWithKey (export sessions) players
|
room (T {players, sessions}) = mapWithKey (export sessions) players
|
||||||
toEncoding (T {players, sessions}) = toEncoding $ mapWithKey (export sessions) players
|
|
||||||
|
|
||||||
new :: T
|
new :: T
|
||||||
new = T {
|
new = T {
|
||||||
|
@ -81,41 +72,41 @@ new = T {
|
||||||
, games = Map.empty
|
, games = Map.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
register :: forall a b. (Enum a, Ord a, Data.RW (Map a b) T) => b -> T -> (T, a)
|
register :: forall a b. (Enum a, Ord a, RW.RW (Map a b) T) => b -> T -> (T, a)
|
||||||
register x server =
|
register x server =
|
||||||
let key = maybe (toEnum 0) (\(n, _) -> succ n) $ lookupMax $ (Data.get server :: Map a b) in
|
let playerID = maybe (toEnum 0) (\(n, _) -> succ n) $ lookupMax $ (RW.get server :: Map a b) in
|
||||||
(Data.update (insert key x) server, key)
|
(RW.update (insert playerID x) server, playerID)
|
||||||
|
|
||||||
get :: forall a b. (Ord a, Data.RW (Map a b) T) => a -> T -> b
|
get :: forall a b. (Ord a, RW.RW (Map a b) T) => a -> T -> b
|
||||||
get key server = (Data.get server :: Map a b) ! key
|
get playerID server = (RW.get server :: Map a b) ! playerID
|
||||||
|
|
||||||
update :: forall a b. (Ord a, Data.RW (Map a b) T) => a -> (b -> b) -> T -> T
|
update :: forall a b. (Ord a, RW.RW (Map a b) T) => a -> (b -> b) -> T -> T
|
||||||
update key updator =
|
update playerID updator =
|
||||||
Data.update (adjust updator key :: Map a b -> Map a b)
|
RW.update (adjust updator playerID :: Map a b -> Map a b)
|
||||||
|
|
||||||
disconnect :: Player.Key -> T -> T
|
disconnect :: PlayerID -> T -> T
|
||||||
disconnect key =
|
disconnect playerID =
|
||||||
Data.update (delete key :: Sessions -> Sessions) . logOut key
|
RW.update (delete playerID :: Sessions -> Sessions) . logOut playerID
|
||||||
|
|
||||||
endGame :: Game.Key -> T -> T
|
endGame :: GameID -> T -> T
|
||||||
endGame key =
|
endGame playerID =
|
||||||
Data.update (delete key :: Games -> Games)
|
RW.update (delete playerID :: Games -> Games)
|
||||||
|
|
||||||
logIn :: Text -> Player.Key -> T -> Either String T
|
logIn :: Text -> PlayerID -> T -> Either String T
|
||||||
logIn name key server =
|
logIn name playerID server =
|
||||||
Data.update (Set.insert name) .
|
RW.update (Set.insert name) .
|
||||||
Data.update (insert key $ Player.T {Player.name}) .
|
RW.update (insert playerID name) .
|
||||||
update key (Data.set $ Session.LoggedIn True :: Session.Update) <$>
|
update playerID (RW.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 :: Player.Key -> T -> T
|
logOut :: PlayerID -> T -> T
|
||||||
logOut key server =
|
logOut playerID server =
|
||||||
maybe
|
maybe
|
||||||
server
|
server
|
||||||
(\player ->
|
(\playerName ->
|
||||||
Data.update (delete key :: Players -> Players) $
|
RW.update (delete playerID :: Players -> Players) $
|
||||||
update key (Data.set $ Session.LoggedIn False :: Session.Update) $
|
update playerID (RW.set $ Session.LoggedIn False :: Session.Update) $
|
||||||
Data.update (Set.delete $ Player.name player :: Names -> Names) server)
|
RW.update (Set.delete playerName :: Names -> Names) server)
|
||||||
(players server !? key)
|
(players server !? playerID)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
|
||||||
module Session (
|
module Session (
|
||||||
Status(..)
|
Status(..)
|
||||||
, T(..)
|
, T(..)
|
||||||
|
@ -8,23 +7,16 @@ module Session (
|
||||||
, open
|
, open
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import Hanafuda.KoiKoi (GameID, PlayerID)
|
||||||
import Network.WebSockets (Connection)
|
import Network.WebSockets (Connection)
|
||||||
import Data.Aeson (ToJSON(..), genericToEncoding)
|
import qualified RW (RW(..))
|
||||||
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 Player.Key
|
| Answering PlayerID
|
||||||
| Waiting Player.Key
|
| Waiting PlayerID
|
||||||
| Playing Game.Key
|
| Playing GameID
|
||||||
deriving (Show, Generic)
|
deriving (Show)
|
||||||
|
|
||||||
instance ToJSON Status where
|
|
||||||
toEncoding = genericToEncoding JSON.singleLCField
|
|
||||||
|
|
||||||
data T = T {
|
data T = T {
|
||||||
connection :: Connection
|
connection :: Connection
|
||||||
|
@ -32,7 +24,7 @@ data T = T {
|
||||||
}
|
}
|
||||||
type Update = T -> T
|
type Update = T -> T
|
||||||
|
|
||||||
instance Data.RW Status T where
|
instance RW.RW Status T where
|
||||||
get = status
|
get = status
|
||||||
set status session = session {status}
|
set status session = session {status}
|
||||||
|
|
||||||
|
|
172
www/game.js
172
www/game.js
|
@ -9,17 +9,9 @@ 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) {
|
||||||
|
@ -38,17 +30,35 @@ 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() {queue = [];})
|
modules.async.apply(function() {
|
||||||
|
queue = queue.slice(length);
|
||||||
|
runQueue();
|
||||||
|
})
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGameMessage(o) {
|
function handleGameMessage(o) {
|
||||||
if(o.game.deck == 24) {
|
if(o.game.deck == 24) { // deck is full, means new round
|
||||||
return o.logs.length > 0 ? modules.async.sequence(applyDiff(o), setGame(o)) : setGame(o);
|
if(o.logs.length > 0) { // but still some logs, from the previous round
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
@ -69,39 +79,75 @@ function Game(modules) {
|
||||||
|
|
||||||
function handleStep(o) {
|
function handleStep(o) {
|
||||||
return function(f) {
|
return function(f) {
|
||||||
if(status.step == "Turned") {
|
handleTurnedCard(o, f);
|
||||||
setTurned(o.game.step.contents);
|
if(status.step == "Scored") {
|
||||||
} else {
|
if(status.playing) {
|
||||||
if(status.step == "ToPlay" && o.game.playing == o.game.oyake) {
|
askKoikoi(o, f);
|
||||||
rest.className = ["card", "count" + o.game.deck].join(' ');
|
} else {
|
||||||
}
|
theyScored(o, f);
|
||||||
if(deck.lastChild.id != "rest") {
|
}
|
||||||
deck.removeChild(deck.lastChild);
|
} else if (status.step == "Over") {
|
||||||
}
|
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(
|
||||||
|
@ -129,7 +175,8 @@ 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(card.name);
|
cardSet[card.name] = turnedCard || new 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;
|
||||||
|
@ -144,7 +191,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];
|
||||||
|
@ -255,18 +302,11 @@ 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(cardName);
|
turnedCard = new TurnedCard(cardName);
|
||||||
if(status.playing) {
|
if(status.playing) {
|
||||||
selected = cardName;
|
selected = turnedCard;
|
||||||
showCandidates(modules.hanafuda.Card[selected], true);
|
showCandidates(modules.hanafuda.Card[cardName], true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +314,12 @@ 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;
|
||||||
|
@ -298,9 +344,8 @@ function Game(modules) {
|
||||||
var card = this;
|
var card = this;
|
||||||
return function() {
|
return function() {
|
||||||
if(card.candidate) {
|
if(card.candidate) {
|
||||||
var withCard = selected;
|
var withCard = selected.name;
|
||||||
selected = null;
|
selected.setSelected(false);
|
||||||
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}
|
||||||
);
|
);
|
||||||
|
@ -313,6 +358,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -322,7 +376,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) {
|
||||||
sets.you.hand.card[selected].setSelected(false);
|
selected.setSelected(false);
|
||||||
} else {
|
} else {
|
||||||
card.play();
|
card.play();
|
||||||
}
|
}
|
||||||
|
@ -330,11 +384,7 @@ function Game(modules) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
HandCard.prototype.setSelected = function(yes) {
|
HandCard.prototype.setSelected = setSelected;
|
||||||
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);
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<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>
|
||||||
|
@ -69,6 +70,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="dialog">
|
<div id="dialog">
|
||||||
</div>
|
</div>
|
||||||
<p id="debug"></p>
|
<p id="error"></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -56,6 +56,7 @@ 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
|
||||||
|
|
|
@ -5,12 +5,13 @@ 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();
|
var messaging = Messaging({screen: screen});
|
||||||
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 login = Login({dom: dom, i18n: i18n, messaging: messaging, room: room, screen: screen, session: session});
|
var statusHandler = StatusHandler();
|
||||||
|
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});
|
var game = Game({async: async, dom: dom, i18n: i18n, fun: fun, hanafuda: hanafuda, messaging: messaging, room: room, screen: screen, session: session, statusHandler: statusHandler});
|
||||||
|
|
||||||
var domElems = {
|
var domElems = {
|
||||||
join: document.getElementById('login').join,
|
join: document.getElementById('login').join,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
function Messaging(screen) {
|
function Messaging(modules) {
|
||||||
var ws = new WebSocket('ws://' + window.location.hostname + '/play/');
|
var ws = new WebSocket(window.location.origin.replace(/^http/, 'ws') + '/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: {}};
|
||||||
|
|
||||||
|
@ -25,6 +27,15 @@ function Messaging(screen) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
@ -42,18 +53,29 @@ function Messaging(screen) {
|
||||||
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 {
|
||||||
debug.textContent = event.data;
|
console.log("No route found for " + event.data);
|
||||||
}
|
}
|
||||||
|
o.direction = 'client < server';
|
||||||
|
log(o);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
if(doLog) {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
ping();
|
|
||||||
addEventListener(["Pong"], ping);
|
|
||||||
ws.addEventListener('message', messageListener);
|
ws.addEventListener('message', messageListener);
|
||||||
|
ws.addEventListener('open', ping);
|
||||||
|
addEventListener(["Pong"], ping);
|
||||||
|
addEventListener(["Error"], function(o) {modules.screen.error(o.error);});
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
||||||
|
|
|
@ -33,3 +33,22 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
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
|
||||||
};
|
};
|
||||||
|
@ -35,4 +40,9 @@ function Screen(modules) {
|
||||||
layer.appendChild(dialog);
|
layer.appendChild(dialog);
|
||||||
layer.className = "on";
|
layer.className = "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function error(message) {
|
||||||
|
errorBox.textContent = message;
|
||||||
|
errorBox.className = "on";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
20
www/statusHandler.js
Normal file
20
www/statusHandler.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ 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) {
|
||||||
|
@ -24,6 +25,7 @@ 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;
|
||||||
},
|
},
|
||||||
|
@ -37,6 +39,7 @@ 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 ?"
|
||||||
},
|
},
|
||||||
|
@ -57,6 +60,7 @@ 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) {
|
||||||
|
@ -64,6 +68,7 @@ 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;
|
||||||
},
|
},
|
||||||
|
@ -77,6 +82,7 @@ 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 ?"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue