Compare commits

...

27 commits

Author SHA1 Message Date
e2019e2d3b Implement a partial ordering on game coordinates to allow implementing re-synchronization easily 2020-01-25 11:13:56 +01:00
59c467e988 Add FromClient messages to handle game sync at reconnection and move logs into PublicGame data structure for simplicity 2020-01-20 22:57:13 +01:00
4c404df4d6 Generalize GameState into a concept of game Coordinates, moving the «month» property into it 2020-01-18 09:34:10 +01:00
509a5c453f Take game ID with turn into a GameState sub-structure shared between private and public parts to prevent replay-attack 2020-01-13 08:31:47 +01:00
413d6cfc84 Merge branch 'main' into stateless-game 2020-01-04 12:09:06 +01:00
1dd31d7091 Make reading IDs safe and provide useful debug messages 2019-12-27 17:55:42 +01:00
c6352a9669 Add GameIDs to PublicGame 2019-12-27 17:55:14 +01:00
b62dc4ff28 LogOut becomes a server message (clients just shut the connection now) 2019-12-24 00:36:17 +01:00
d32e61f927 LogIn is back, but now it's a Server message 2019-12-08 22:57:57 +01:00
ce31683fee Generalize the definition of (From|To)JSON(|Key) to all ID types having a prefix as per the newest changes in the lib 2019-11-20 18:23:57 +01:00
f968c41d9d Follow latest changes in Hanafuda.ID 2019-11-12 22:18:52 +01:00
57370d62ee Throw a couple of Show instance to ease debugging following the e0003c commit in lib 2019-11-12 22:18:33 +01:00
775abd3ac4 No need to separate connection and logIn now that names are non-unique and the PlayerID is known from the start 2019-11-05 18:13:13 +01:00
9c5c80fb3a Still working on the protocol : remove useless PlayerStatus type 2019-10-28 08:18:48 +01:00
816ecbc331 Start removing deprecated data from protocol messages 2019-10-23 17:46:33 +02:00
43b0200304 Store both hands in private state because it's easier to handle and allow the player's hand to remain unsigned (they can modify it, it's not taken into account by the server) 2019-10-22 17:45:07 +02:00
8538b8f5ea Store the player turns in the public state of games 2019-10-22 17:43:33 +02:00
e10afdfac1 Rename fields for lighter syntax 2019-10-17 19:49:50 +02:00
189e29b08c Add a field to store the number of turns played in a round 2019-10-17 19:24:51 +02:00
818c343e89 Use Base64 to correctly handle binary ByteStrings 2019-10-17 18:53:11 +02:00
0c2974b055 Add needed JSON instances for PrivateState 2019-10-17 12:13:15 +02:00
5cccd05290 Use new handier type synonyms from lib 2019-10-17 12:12:51 +02:00
e11a899745 Apparently a nonce will be needed but can be transmitted unciphered along with the message 2019-10-17 11:24:36 +02:00
9cfcd691fd Export the previously defined data types 2019-10-16 18:47:40 +02:00
c4edae9781 Define a public type for players and keep only the player's hand in private information and public game 2019-10-16 18:44:46 +02:00
65a7c26b5a Player will need to pass the state of the game along with their move since the server won't remember it for them 2019-10-16 18:43:53 +02:00
2f7be7df2b Implement a separate public type for games to cipher a part of its content and sign another 2019-10-16 10:31:30 +02:00
2 changed files with 119 additions and 43 deletions

View file

@ -23,6 +23,8 @@ library
-- other-extensions:
build-depends: aeson
, base >=4.9 && <4.13
, base64-bytestring
, bytestring
, containers
, hanafuda >= 0.3.3
, text

View file

@ -3,42 +3,56 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Hanafuda.Message (
T(..)
, FromClient(..)
, PlayerStatus(..)
, PublicGame
, Coordinates(..)
, PrivateState(..)
, PublicGame(..)
, PublicPlayer(..)
, PublicState(..)
, Room
, orderCoordinates
) where
import Data.Char (toLower)
import Data.Aeson (
FromJSON(..), FromJSONKey(..), FromJSONKeyFunction(..), Options(..), SumEncoding(..), ToJSON(..), ToJSONKey(..)
, Value, (.:), (.=), defaultOptions, eitherDecode', encode, genericParseJSON
, genericToEncoding, object, pairs, withObject
, Value(..), (.:), (.=), defaultOptions, eitherDecode', encode, genericParseJSON
, genericToEncoding, object, pairs, withObject, withText
)
import Data.Aeson.Types (toJSONKeyText)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Base64 as B64 (decode, encode)
import Data.Map (Map)
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as Text (pack, unpack)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import GHC.Generics (Generic)
import qualified Hanafuda (Card(..), Flower(..), Pack, cardsOfPack, empty, packOfCards)
import Hanafuda.ID (ID(..), getID)
import Hanafuda.KoiKoi (PlayerID, GameBlueprint(..))
import Hanafuda.ID (ID(..), IDType(..), Prefix(..))
import Hanafuda.KoiKoi (GameID, PlayerID)
import qualified Hanafuda.KoiKoi as KoiKoi (
Action(..), Game(..), Mode(..), Move(..), Score, Source(..), Step(..), Yaku(..)
Action(..), Game(..), Mode(..), Move(..), Player(..), Players(..), PlayerTurn
, Score, Scores, Source(..), Step(..), Yaku(..)
)
import Hanafuda.Player (Player(..), Players(..))
import Text.Read (readMaybe)
deriving instance Generic PlayerID
instance FromJSON PlayerID
instance FromJSONKey PlayerID where
fromJSONKey = FromJSONKeyText (ID . read . Text.unpack)
instance ToJSON PlayerID where
toEncoding = genericToEncoding defaultOptions
instance ToJSONKey PlayerID where
toJSONKey = toJSONKeyText (Text.pack . getID)
instance IDType a => FromJSON (ID a) where
parseJSON = withText decoding (safeRead . Text.unpack)
where
decoding = let Prefix p = (prefix :: Prefix a) in p ++ "ID"
safeRead s = maybe (fail $ "Not an ID: '" ++ s ++ "'") return $ readMaybe s
instance IDType a => FromJSONKey (ID a) where
fromJSONKey = FromJSONKeyText (read . Text.unpack)
instance IDType a => ToJSON (ID a) where
toJSON = toJSON . show
toEncoding = toEncoding . show
instance IDType a => ToJSONKey (ID a) where
toJSONKey = toJSONKeyText (Text.pack . show)
first :: (a -> a) -> [a] -> [a]
first _ [] = []
@ -62,32 +76,23 @@ instance ToJSON Hanafuda.Card where
toEncoding = genericToEncoding defaultOptions
data FromClient =
Answer {accept :: Bool}
Hello {name :: Text}
| Tadaima {myID :: PlayerID, name :: Text}
| Answer {accept :: Bool, to :: PlayerID}
| Invitation {to :: PlayerID}
| LogIn {name :: Text}
| LogOut
| Play {move :: KoiKoi.Move}
| Play {move :: KoiKoi.Move, onGame :: PublicGame}
| Share {gameSave :: PublicGame}
| Sync {latestKnown :: Coordinates, to :: PlayerID}
| Yield {onGameID :: GameID, to :: PlayerID}
| Quit
| Ping
deriving (Generic)
deriving (Generic, Show)
instance FromJSON FromClient
instance ToJSON FromClient where
toEncoding = genericToEncoding defaultOptions
newtype PlayerStatus = PlayerStatus (Text, Bool) deriving (Generic, Show)
instance FromJSON PlayerStatus where
parseJSON = withObject "PlayerStatus" $ \v -> fmap PlayerStatus . (,)
<$> v .: "name"
<*> v .: "alone"
instance ToJSON PlayerStatus where
toJSON (PlayerStatus (name, alone)) =
object ["name" .= name, "alone" .= alone]
toEncoding (PlayerStatus (name, alone)) =
pairs ("name" .= name <> "alone" .= alone)
type Room = Map PlayerID PlayerStatus
type Room = Map PlayerID Text
deriving instance Generic KoiKoi.Source
instance FromJSON KoiKoi.Source
@ -108,14 +113,14 @@ instance ToJSON KoiKoi.Yaku where
instance ToJSONKey KoiKoi.Yaku where
toJSONKey = toJSONKeyText (Text.pack . show)
deriving instance Generic (Player KoiKoi.Score)
instance FromJSON (Player KoiKoi.Score)
instance ToJSON (Player KoiKoi.Score) where
deriving instance Generic KoiKoi.Player
instance FromJSON KoiKoi.Player
instance ToJSON KoiKoi.Player where
toEncoding = genericToEncoding defaultOptions
deriving instance Generic (Players KoiKoi.Score)
instance FromJSON (Players KoiKoi.Score)
instance ToJSON (Players KoiKoi.Score) where
deriving instance Generic KoiKoi.Players
instance FromJSON KoiKoi.Players
instance ToJSON KoiKoi.Players where
toEncoding = genericToEncoding defaultOptions
deriving instance Generic KoiKoi.Step
@ -139,9 +144,76 @@ instance FromJSON Hanafuda.Flower
instance ToJSON Hanafuda.Flower where
toEncoding = genericToEncoding defaultOptions
type PublicGame = GameBlueprint Int
data Coordinates = Coordinates {
gameID :: GameID
, month :: Hanafuda.Flower
, turn :: Int
} deriving (Eq, Generic, Show)
instance FromJSON Coordinates
instance ToJSON Coordinates where
toEncoding = genericToEncoding defaultOptions
orderCoordinates :: Coordinates -> Coordinates -> Maybe Ordering
orderCoordinates coordinatesA coordinatesB
| gameID coordinatesA == gameID coordinatesB = Just $ compare
(month coordinatesA, turn coordinatesA)
(month coordinatesB, turn coordinatesB)
| otherwise = Nothing
data PrivateState = PrivateState {
link :: Coordinates
, hands :: Map PlayerID Hanafuda.Pack
, deck :: [Hanafuda.Card]
} deriving Generic
instance FromJSON PrivateState
instance ToJSON PrivateState where
toEncoding = genericToEncoding defaultOptions
data PublicPlayer = PublicPlayer {
meld :: Hanafuda.Pack
, yakus :: KoiKoi.Score
} deriving (Generic, Show)
instance FromJSON PublicPlayer
instance ToJSON PublicPlayer where
toEncoding = genericToEncoding defaultOptions
data PublicState = PublicState {
coordinates :: Coordinates
, mode :: KoiKoi.Mode
, scores :: KoiKoi.Scores
, nextPlayer :: KoiKoi.PlayerTurn
, players :: Map PlayerID PublicPlayer
, playing :: PlayerID
, winning :: PlayerID
, oyake :: PlayerID
, river :: Hanafuda.Pack
, step :: KoiKoi.Step
, trick :: [Hanafuda.Card]
, rounds :: [(PlayerID, KoiKoi.Score)]
} deriving (Generic, Show)
instance FromJSON PublicState
instance ToJSON PublicState where
toEncoding = genericToEncoding defaultOptions
instance FromJSON ByteString where
parseJSON = withText "ByteString" (either fail return . B64.decode . encodeUtf8)
instance ToJSON ByteString where
toJSON = String . decodeUtf8 . B64.encode
toEncoding = toEncoding . decodeUtf8 . B64.encode
data PublicGame = PublicGame {
nonce :: ByteString
, logs :: [KoiKoi.Action]
, playerHand :: Hanafuda.Pack
, private :: ByteString
, public :: PublicState
, publicSignature :: ByteString
} deriving (Generic, Show)
deriving instance Generic PublicGame
instance FromJSON PublicGame
instance ToJSON PublicGame where
toEncoding = genericToEncoding defaultOptions
@ -149,8 +221,10 @@ instance ToJSON PublicGame where
data T =
Relay {from :: PlayerID, message :: FromClient}
| Welcome {room :: Room, key :: PlayerID}
| Update {alone :: [PlayerID], paired :: [PlayerID]}
| Game {game :: PublicGame, logs :: [KoiKoi.Action]}
| Okaeri {room :: Room}
| LogIn {from :: PlayerID, as :: Text}
| LogOut {from :: PlayerID}
| Game {state :: PublicGame}
| Pong
| Error {error :: String}
deriving (Generic)