Compare commits

...

6 Commits

16 changed files with 123 additions and 86 deletions

4
.gitignore vendored
View File

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

View File

@ -26,7 +26,7 @@ executable hanafudapi
, Config
, Messaging
, Game
, Data
, RW
, Server
, Session
-- other-extensions:

View File

@ -12,13 +12,13 @@ module App (
, update_
) where
import Data.Map ((!))
import Control.Concurrent (MVar, modifyMVar, putMVar, readMVar, takeMVar)
import Control.Monad.Reader (ReaderT(..), ask, asks, lift)
import Network.WebSockets (Connection)
import Data.Map ((!))
import Hanafuda.KoiKoi (PlayerID)
import qualified Session (T(..))
import Network.WebSockets (Connection)
import qualified Server (T(..))
import qualified Session (T(..))
data Context = Context {
mServer :: MVar Server.T

View File

@ -3,20 +3,20 @@ module Automaton (
start
) where
import qualified App (Context(..), T, current, debug, get, server, try, update_)
import Control.Monad.Reader (asks)
import qualified Data (RW(..))
import Data.Map (Map, (!?))
import qualified Game (new, play)
import qualified Hanafuda.KoiKoi as KoiKoi (
Game, GameBlueprint(..), GameID, Step(..)
)
import qualified Session (Status(..), T(..), Update)
import qualified Server (endGame, get, logIn, logOut, update, room)
import qualified App (Context(..), T, current, debug, get, server, try, update_)
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)
receive :: Session.Status -> Message.FromClient -> App.T ()
@ -36,7 +36,7 @@ receive (Session.LoggedIn True) invitation@(Message.Invitation {Message.to}) = d
case Session.status session of
Session.LoggedIn True -> do
from <- asks App.playerID
App.update_ (Server.update to (Data.set $ Session.Answering from :: Session.Update))
App.update_ (Server.update to (RW.set $ Session.Answering from :: Session.Update))
Messaging.broadcast $ Messaging.update {Message.paired = [from, to]}
(Messaging.relay invitation $ Messaging.sendTo [to])
setSessionStatus (Session.Waiting to)
@ -58,7 +58,7 @@ receive (Session.Answering to) message@(Message.Answer {Message.accept}) = do
else do
Messaging.broadcast $ Messaging.update {Message.alone = [for, to]}
return $ Session.LoggedIn True
App.update_ $ Server.update to (Data.set newStatus :: Session.Update)
App.update_ $ Server.update to (RW.set newStatus :: Session.Update)
setSessionStatus newStatus
_ -> sendError "They're not waiting for your answer"
@ -77,7 +77,7 @@ receive (Session.Playing gameID) played@(Message.Play {}) = do
Messaging.notifyPlayers newGame logs
receive (Session.Playing gameID) Message.Quit = do
games <- (Data.get <$> App.server :: App.T (Map KoiKoi.GameID KoiKoi.Game))
games <- (RW.get <$> App.server :: App.T (Map KoiKoi.GameID KoiKoi.Game))
case games !? gameID of
Nothing -> do
playerID <- asks App.playerID
@ -93,7 +93,7 @@ sendError = Messaging.send . Message.Error
setSessionStatus :: Session.Status -> App.T ()
setSessionStatus newStatus = do
playerID <- asks App.playerID
App.update_ $ Server.update playerID $ (Data.set newStatus :: Session.Update)
App.update_ $ Server.update playerID $ (RW.set newStatus :: Session.Update)
App.debug $ show newStatus
loop :: App.T ()

View File

@ -10,12 +10,12 @@ import Control.Monad.Reader (lift)
import Control.Monad.Writer (runWriterT)
import Data.Map (mapWithKey)
import qualified Hanafuda (empty)
import Hanafuda.KoiKoi (Game, GameBlueprint(..), GameID, Mode(..), PlayerID)
import qualified Hanafuda.KoiKoi as KoiKoi (
Action, Move(..), play, new
)
import Hanafuda.KoiKoi (Game, GameBlueprint(..), GameID, Mode(..), PlayerID)
import qualified Hanafuda.Player (Player(..), Players(..))
import Hanafuda.Message (PublicGame)
import qualified Hanafuda.Player (Player(..), Players(..))
import qualified Server (register)
new :: (PlayerID, PlayerID) -> App.T GameID

View File

@ -2,21 +2,21 @@
{-# LANGUAGE NamedFieldPuns #-}
module Main where
import Network.Wai.Handler.Warp (run)
import Network.HTTP.Types.Status (badRequest400)
import Network.WebSockets (ServerApp, acceptRequest, defaultConnectionOptions)
import Network.Wai.Handler.WebSockets (websocketsOr)
import Network.Wai (responseLBS)
import Control.Monad.Reader (ReaderT(..), asks)
import qualified App (Context(..), T, update_)
import qualified Automaton (start)
import qualified Config (listenPort)
import Control.Concurrent (newMVar, modifyMVar)
import Control.Exception (finally)
import qualified Config (listenPort)
import qualified Session (open)
import qualified Server (disconnect, new, register)
import qualified App (Context(..), T, update_)
import Control.Monad.Reader (ReaderT(..), asks)
import qualified Hanafuda.Message as Message (FromClient(..))
import Messaging (broadcast, relay)
import qualified Automaton (start)
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)
exit :: App.T ()
exit = do

View File

@ -13,20 +13,20 @@ module Messaging (
, update
) where
import Data.List (intercalate)
import Data.Foldable (forM_)
import Data.Map (keys)
import Data.Aeson (eitherDecode', encode)
import Network.WebSockets (receiveData, sendTextData)
import Data.ByteString.Lazy.Char8 (unpack)
import Control.Monad.Reader (asks, lift)
import qualified Game (export)
import qualified Session (T(..))
import qualified Server (T(..), get)
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 qualified Hanafuda.Message as Message (T)
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

View File

@ -1,5 +1,5 @@
{-# LANGUAGE MultiParamTypeClasses #-}
module Data (
module RW (
RW(..)
) where

View File

@ -23,7 +23,7 @@ import qualified Data.Set as Set (delete, empty, insert)
import Data.Text (Text)
import Hanafuda.KoiKoi (Game, GameID, PlayerID)
import Hanafuda.Message (PlayerStatus(..), Room)
import qualified Data (RW(..))
import qualified RW (RW(..))
import qualified Session (Status(..), T(..), Update)
type Names = Set Text
@ -37,19 +37,19 @@ data T = T {
, games :: Games
}
instance Data.RW Names T where
instance RW.RW Names T where
get = names
set names server = server {names}
instance Data.RW Players T where
instance RW.RW Players T where
get = players
set players server = server {players}
instance Data.RW Sessions T where
instance RW.RW Sessions T where
get = sessions
set sessions server = server {sessions}
instance Data.RW Games T where
instance RW.RW Games T where
get = games
set games server = server {games}
@ -72,31 +72,31 @@ new = T {
, 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 =
let playerID = maybe (toEnum 0) (\(n, _) -> succ n) $ lookupMax $ (Data.get server :: Map a b) in
(Data.update (insert playerID x) server, playerID)
let playerID = maybe (toEnum 0) (\(n, _) -> succ n) $ lookupMax $ (RW.get server :: Map a b) in
(RW.update (insert playerID x) server, playerID)
get :: forall a b. (Ord a, Data.RW (Map a b) T) => a -> T -> b
get playerID server = (Data.get server :: Map a b) ! playerID
get :: forall a b. (Ord a, RW.RW (Map a b) T) => a -> T -> b
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 playerID updator =
Data.update (adjust updator playerID :: Map a b -> Map a b)
RW.update (adjust updator playerID :: Map a b -> Map a b)
disconnect :: PlayerID -> T -> T
disconnect playerID =
Data.update (delete playerID :: Sessions -> Sessions) . logOut playerID
RW.update (delete playerID :: Sessions -> Sessions) . logOut playerID
endGame :: GameID -> T -> T
endGame playerID =
Data.update (delete playerID :: Games -> Games)
RW.update (delete playerID :: Games -> Games)
logIn :: Text -> PlayerID -> T -> Either String T
logIn name playerID server =
Data.update (Set.insert name) .
Data.update (insert playerID name) .
update playerID (Data.set $ Session.LoggedIn True :: Session.Update) <$>
RW.update (Set.insert name) .
RW.update (insert playerID name) .
update playerID (RW.set $ Session.LoggedIn True :: Session.Update) <$>
if name `member` names server
then Left "This name is already registered"
else Right server
@ -106,7 +106,7 @@ logOut playerID server =
maybe
server
(\playerName ->
Data.update (delete playerID :: Players -> Players) $
update playerID (Data.set $ Session.LoggedIn False :: Session.Update) $
Data.update (Set.delete playerName :: Names -> Names) server)
RW.update (delete playerID :: Players -> Players) $
update playerID (RW.set $ Session.LoggedIn False :: Session.Update) $
RW.update (Set.delete playerName :: Names -> Names) server)
(players server !? playerID)

View File

@ -7,9 +7,9 @@ module Session (
, open
) where
import Network.WebSockets (Connection)
import Hanafuda.KoiKoi (GameID, PlayerID)
import qualified Data (RW(..))
import Network.WebSockets (Connection)
import qualified RW (RW(..))
data Status =
LoggedIn Bool
@ -24,7 +24,7 @@ data T = T {
}
type Update = T -> T
instance Data.RW Status T where
instance RW.RW Status T where
get = status
set status session = session {status}

View File

@ -9,6 +9,7 @@ function Game(modules) {
};
var sets = buildSets();
var selected = null;
var turnedCard = null;
var queue = [];
function buildSets() {
@ -174,7 +175,8 @@ function Game(modules) {
movingCards.push([sets[side].hand, dest, card]);
} else {
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]);
}
return movingCards;
@ -189,7 +191,7 @@ function Game(modules) {
function moveCard(fromSet, toSet, card) {
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) {
originalCard = fromSet.card[card.name].dom;
delete fromSet.card[card.name];
@ -300,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) {
turnedCard(cardName);
turnedCard = new TurnedCard(cardName);
if(status.playing) {
selected = cardName;
showCandidates(modules.hanafuda.Card[selected], true);
selected = turnedCard;
showCandidates(modules.hanafuda.Card[cardName], true);
}
}
@ -319,6 +314,12 @@ function Game(modules) {
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) {
this.value = modules.hanafuda.Card[name];
this.name = name;
@ -343,9 +344,8 @@ function Game(modules) {
var card = this;
return function() {
if(card.candidate) {
var withCard = selected;
selected = null;
showCandidates(card.value, false);
var withCard = selected.name;
selected.setSelected(false);
play(
status.step == 'ToPlay' ? {capture: [withCard, card.name]} : {choose: card.name}
);
@ -358,6 +358,15 @@ function Game(modules) {
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() {
Card.apply(this, arguments);
}
@ -367,7 +376,7 @@ function Game(modules) {
return function() {
if(status.playing && status.step == "ToPlay") {
if(selected != undefined) {
sets.you.hand.card[selected].setSelected(false);
selected.setSelected(false);
} else {
card.play();
}
@ -375,11 +384,7 @@ function Game(modules) {
};
};
HandCard.prototype.setSelected = function(yes) {
selected = yes ? this.name : null;
this.dom.classList.toggle('selected', yes);
showCandidates(this.value, yes);
}
HandCard.prototype.setSelected = setSelected;
HandCard.prototype.play = function() {
var matching = matchingInRiver(this.value);

View File

@ -70,6 +70,6 @@
</div>
<div id="dialog">
</div>
<p id="debug"></p>
<p id="error"></p>
</body>
</html>

View File

@ -5,7 +5,7 @@ window.addEventListener('load', function() {
var i18n = I18n({translations: translations});
var fun = Fun();
var screen = Screen({dom: dom, i18n: i18n});
var messaging = Messaging();
var messaging = Messaging({screen: screen});
var session = Session({messaging: messaging});
var room = Room({dom: dom, messaging: messaging, session: session, fun: fun});
var statusHandler = StatusHandler();

View File

@ -1,4 +1,4 @@
function Messaging(screen) {
function Messaging(modules) {
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);
@ -53,7 +53,7 @@ function Messaging(screen) {
if(route != undefined && route.callbacks != undefined) {
route.callbacks.forEach(function(f) {f(o);});
} else {
debug.textContent = event.data;
console.log("No route found for " + event.data);
}
o.direction = 'client < server';
log(o);
@ -69,6 +69,7 @@ function Messaging(screen) {
ws.addEventListener('message', messageListener);
ws.addEventListener('open', ping);
addEventListener(["Pong"], ping);
addEventListener(["Error"], function(o) {modules.screen.error(o.error);});
}
function send(o) {

View File

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