Add ownership for configuration.

This commit is contained in:
Gareth Latty
2016-07-27 22:26:58 +01:00
parent 941523d106
commit 228e08dba6
18 changed files with 144 additions and 61 deletions
+5 -3
View File
@@ -12,6 +12,7 @@ import massivedecks.exceptions._
import massivedecks.models.Errors
import massivedecks.models.Game.Formatters._
import massivedecks.models.Lobby.Formatters._
import massivedecks.models.Lobby.GameCodeAndSecret
import massivedecks.models.Player
import massivedecks.models.Player.Formatters._
import massivedecks.stores.LobbyStore
@@ -19,11 +20,12 @@ import play.api.libs.streams.Streams
class API @Inject()(store: LobbyStore) (implicit context: ExecutionContext) extends Controller {
def createLobby() = Action {
def createLobby() = Action(parse.json) { request: Request[JsValue] =>
wrap {
val gameCode = store.newLobby()
val name = (request.body \ "name").validate[String].getOrElse(throw BadRequestException(Errors.InvalidName()))
val gameCode = store.newLobby(name)
store.performInLobby(gameCode) { lobby =>
Json.toJson(lobby.lobby)
Json.toJson(GameCodeAndSecret(lobby.gameCode, lobby.owner))
}
}
}
+12 -2
View File
@@ -1,15 +1,16 @@
package massivedecks.lobby
import massivedecks.models.Game.{Config => ConfigModel}
import massivedecks.notifications.Notifiers
import massivedecks.models.cardcast.CardcastDeck
import massivedecks.notifications.Notifiers
class Config(notifiers: Notifiers) {
var decks: List[CardcastDeck] = List()
var houseRules: Set[String] = Set()
var password: Option[String] = None
def config = ConfigModel(decks.map(deck => deck.info), houseRules)
def config = ConfigModel(decks.map(deck => deck.info), houseRules, password)
def addDeck(deck: CardcastDeck): Unit = {
decks = decks :+ deck
@@ -26,4 +27,13 @@ class Config(notifiers: Notifiers) {
notifiers.configChange(config)
}
def setPassword(newPass: Option[String]) = {
if (newPass.forall(pw => pw.isEmpty)) {
password = None
} else {
password = newPass
}
notifiers.configChange(config)
}
}
+19 -3
View File
@@ -22,7 +22,7 @@ object Lobby {
* Factory for dependency injection.
*/
class Factory @Inject() (cardcast: CardcastAPI) (implicit context: ExecutionContext) {
def build(gameCode: String) = new Lobby(cardcast, gameCode)
def build(gameCode: String, ownerName: String) = new Lobby(cardcast, gameCode, ownerName)
}
/**
@@ -40,7 +40,7 @@ object Lobby {
* @param cardcast The cardcast api.
* @param gameCode The game code for the lobby.
*/
class Lobby(cardcast: CardcastAPI, val gameCode: String)(implicit context: ExecutionContext) {
class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(implicit context: ExecutionContext) {
/**
* The game in progress if there is one.
@@ -67,10 +67,12 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String)(implicit context: Execu
case Some(g) => GameModel.State.Playing(g.round)
}
val owner = newPlayer(ownerName)
/**
* @return The model for the lobby.
*/
def lobby = LobbyModel.Lobby(gameCode, config.config, players.players, gameState)
def lobby = LobbyModel.Lobby(gameCode, owner.id, config.config, players.players, gameState)
/**
* Add a new player to the lobby.
@@ -95,10 +97,12 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String)(implicit context: Execu
* @param playCode The cardcast play code for the deck.
* @return An empty response.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws RequestFailedException with the key "cardcast-timeout" if the request to cardcast doesn't complete.
*/
def addDeck(secret: Player.Secret, playCode: String): JsValue = {
players.validateSecret(secret)
validateIsOwner(secret)
Try(Await.ready({
cardcast.deck(playCode).map { deck =>
config.addDeck(deck)
@@ -117,9 +121,11 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String)(implicit context: Execu
*
* @param secret The secret of the player making the request.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws ForbiddenException with key "not-owner" if the requester is not the owner.
*/
def newAi(secret: Player.Secret): Unit = {
players.validateSecret(secret)
validateIsOwner(secret)
players.addAi()
}
@@ -318,9 +324,12 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String)(implicit context: Execu
* @param secret The secret of the player making the request.
* @param rule The rule to enable.
* @return An empty response.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws ForbiddenException with key "not-owner" if the requester is not the owner.
*/
def enableRule(secret: Player.Secret, rule: String): JsValue = {
players.validateSecret(secret)
validateIsOwner(secret)
config.addHouseRule(rule)
Json.toJson("")
}
@@ -331,9 +340,12 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String)(implicit context: Execu
* @param secret The secret of the player making the request.
* @param rule The rule to disable.
* @return An empty response.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws ForbiddenException with key "not-owner" if the requester is not the owner.
*/
def disableRule(secret: Player.Secret, rule: String): JsValue = {
players.validateSecret(secret)
validateIsOwner(secret)
config.removeHouseRule(rule)
Json.toJson("")
}
@@ -377,4 +389,8 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String)(implicit context: Execu
case None => throw BadRequestException(Errors.NoGameInProgress())
}
private def validateIsOwner(secret: Player.Secret) = {
ForbiddenException.verify(owner == secret, Errors.NotOwner())
}
}
-8
View File
@@ -26,11 +26,6 @@ class Players(notifiers: Notifiers) {
*/
var connected: Set[Player.Id] = Set()
/**
* The owner of (first player in) the lobby.
*/
var owner: Option[Player.Id] = None
private var secrets: Map[Player.Id, Player.Secret] = Map()
private var nextId: Int = 0
private var aiNames: List[String] = Players.aiName :: Random.shuffle(Players.aiNames)
@@ -55,9 +50,6 @@ class Players(notifiers: Notifiers) {
def addPlayer(name: String): Player.Secret = {
BadRequestException.verify(players.forall(player => player.name != name), Errors.NameInUse())
val id = newId()
if (owner.isEmpty) {
owner = Some(id)
}
val player = Player(id, name)
players = players :+ player
val secret = Player.Secret(id)
+2
View File
@@ -63,4 +63,6 @@ object Errors {
).toString()
}
case class DeckNotFound(error: String = "deck-not-found") extends SimpleErrorDetails
case class NotOwner(error: String = "not-owner") extends SimpleErrorDetails
}
+1 -1
View File
@@ -23,7 +23,7 @@ object Game {
case class DeckInfo(id: String, name: String, calls: Int, responses: Int)
case class Config(decks: List[DeckInfo], houseRules: Set[String])
case class Config(decks: List[DeckInfo], houseRules: Set[String], password: Option[String])
sealed trait State {
def gameState: String
+4 -1
View File
@@ -9,13 +9,16 @@ import massivedecks.models.Player.Formatters._
*/
object Lobby {
case class Lobby(gameCode: String, config: Game.Config, players: List[Player], state: Game.State)
case class Lobby(gameCode: String, owner: Player.Id, config: Game.Config, players: List[Player], state: Game.State)
case class LobbyAndHand(lobby: Lobby, hand: Game.Hand)
case class GameCodeAndSecret(gameCode: String, secret: Player.Secret)
object Formatters {
implicit val lobbyWrites: Writes[Lobby] = Json.writes[Lobby]
implicit val lobbyAndHandWrites: Writes[LobbyAndHand] = Json.writes[LobbyAndHand]
implicit val gameCodeAndSecretWrites: Writes[GameCodeAndSecret] = Json.writes[GameCodeAndSecret]
}
}
+2 -2
View File
@@ -34,11 +34,11 @@ trait CachedStore extends LobbyStore {
*/
protected def storeLobby(lobby: Lobby): Unit
override def newLobby(): String = {
override def newLobby(ownerName: String): String = {
val gameCode = gameCodeManager.generate()
val lock = lockFor(gameCode)
try {
val lobby = lobbyFactory.build(gameCode)
val lobby = lobbyFactory.build(gameCode, ownerName)
saveLobby(lobby)
} finally {
lock.unlock()
+2 -1
View File
@@ -9,9 +9,10 @@ trait LobbyStore {
/**
* Make a new lobby.
* @param ownerName The name of the owner (the player making the lobby).
* @return The game code for the new lobby.
*/
def newLobby(): String
def newLobby(ownerName: String): String
/**
* Perform a read-only action in the lobby.
+2 -2
View File
@@ -15,8 +15,8 @@ import MassiveDecks.Models.JSON.Encode exposing (..)
{-| Makes a request to create a new game lobby to the server. On success, returns that lobby.
-}
createLobby : Request Never Game.Lobby
createLobby = request "POST" "/api/lobbies" Nothing [] lobbyDecoder
createLobby : String -> Request Never Game.GameCodeAndSecret
createLobby name = request "POST" "/api/lobbies" (Just (encodeName name)) [] gameCodeAndSecretDecoder
{-| Errors specific to new player requests.
+3 -1
View File
@@ -24,6 +24,7 @@ type alias GameCode = String
type alias Config =
{ decks : List DeckInfo
, houseRules : List HouseRule.Id
, pasword : Maybe String
}
@@ -47,6 +48,7 @@ type State
-}
type alias Lobby =
{ gameCode : String
, owner : Player.Id
, config : Config
, players : List Player
, game : State
@@ -56,6 +58,6 @@ type alias Lobby =
{-| A lobby and a player's hand.
-}
type alias LobbyAndHand =
{ lobby: Lobby
{ lobby : Lobby
, hand: Card.Hand
}
+10 -2
View File
@@ -16,13 +16,20 @@ lobbyAndHandDecoder = object2 Game.LobbyAndHand
lobbyDecoder : Decoder Game.Lobby
lobbyDecoder = object4 Game.Lobby
lobbyDecoder = object5 Game.Lobby
("gameCode" := string)
("owner" := playerIdDecoder)
("config" := configDecoder)
("players" := (list playerDecoder))
("state" := gameStateDecoder)
gameCodeAndSecretDecoder : Decoder Game.GameCodeAndSecret
gameCodeAndSecretDecoder = object2 Game.GameCodeAndSecret
("gameCode" := string)
("secret" := playerSecretDecoder)
deckInfoDecoder : Decoder Game.DeckInfo
deckInfoDecoder = object4 Game.DeckInfo
("id" := string)
@@ -32,9 +39,10 @@ deckInfoDecoder = object4 Game.DeckInfo
configDecoder : Decoder Game.Config
configDecoder = object2 Game.Config
configDecoder = object3 Game.Config
("decks" := (list deckInfoDecoder))
("houseRules" := (list houseRuleDecoder))
(maybe ("password" := string))
handDecoder : Decoder Card.Hand
@@ -22,6 +22,7 @@ init : Model
init =
{ decks = []
, deckIdInput = Input.initWithExtra DeckId "deck-id-input" UI.deckIdInputLabel "" "Play Code" UI.addDeckButton (Util.cmd AddDeck) InputMessage
, passwordInput = Input.init Password "password-input" UI.passwordInputLabel "" "Password" (Cmd.none) InputMessage
, loadingDecks = []
}
@@ -43,3 +43,4 @@ type alias FailureMessage = String
-}
type InputId
= DeckId
| Password
@@ -10,5 +10,6 @@ import MassiveDecks.Scenes.Config.Messages exposing (Message, InputId)
type alias Model =
{ decks : List Game.DeckInfo
, deckIdInput : Input.Model InputId Message
, passwordInput : Input.Model InputId Message
, loadingDecks : List String
}
+59 -23
View File
@@ -1,4 +1,4 @@
module MassiveDecks.Scenes.Config.UI exposing (view, deckIdInputLabel, addDeckButton)
module MassiveDecks.Scenes.Config.UI exposing (view, deckIdInputLabel, passwordInputLabel, addDeckButton)
import String
@@ -25,9 +25,11 @@ view lobbyModel =
decks = lobby.config.decks
enoughPlayers = ((List.length lobby.players) > 1)
enoughCards = not (List.isEmpty decks)
canNotChangeConfig = lobbyModel.lobby.owner /= lobbyModel.secret.id
in
div [ id "config" ]
[ div [ id "config-content", class "mui-panel" ]
( (if canNotChangeConfig then infoBar else [])
++ [ div [ id "config-content", class "mui-panel" ]
[ invite lobbyModel.init.url lobby.gameCode
, div [ class "mui-divider" ] []
, h1 [] [ text "Game Setup" ]
@@ -37,16 +39,46 @@ view lobbyModel =
[ text "Decks" ]
]
, li [] [ a [ attribute "data-mui-toggle" "tab"
, attribute "data-mui-controls" "house-rules" ] [ text "House Rules" ] ]
]
, attribute "data-mui-controls" "house-rules"
] [ text "House Rules" ]
]
, li [] [ a [ attribute "data-mui-toggle" "tab"
, attribute "data-mui-controls" "lobby-settings"
] [ text "Lobby Settings" ]
]
]
, div [ id "decks", class "mui-tabs__pane mui--is-active" ]
[ deckList decks model.loadingDecks model.deckIdInput ]
[ deckList canNotChangeConfig decks model.loadingDecks model.deckIdInput ]
, div [ id "house-rules", class "mui-tabs__pane" ]
([ rando ] ++ (List.map (\rule -> houseRule (List.member rule.id lobbyModel.lobby.config.houseRules) rule) houseRules))
([ rando canNotChangeConfig ] ++ (List.map (\rule -> houseRule canNotChangeConfig (List.member rule.id lobbyModel.lobby.config.houseRules) rule) houseRules))
, div [ id "lobby-settings", class "mui-tabs__pane" ]
[ password model.passwordInput ]
, div [ class "mui-divider" ] []
, startGameButton enoughPlayers enoughCards
, startGameButton canNotChangeConfig enoughPlayers enoughCards
]
]
])
infoBar : List (Html msg)
infoBar =
[ div [ id "info-bar", class "mui--z1" ]
[ Icon.icon "info-circle"
, text " "
, text "You can't change the configuration of the game, as you are not the owner."
]
]
passwordInputLabel : List (Html msg)
passwordInputLabel = [ text "If blank, no password will be needed - anyone with the game code can play." ]
password : Input.Model InputId Message -> Html Message
password passwordInputModel =
div [] [ h3 [] [ Icon.icon "key", text " Privacy" ]
, p [] [ text "A password that players will need to enter to get in the game." ]
, Input.view passwordInputModel
]
invite : String -> String -> Html msg
invite appUrl lobbyId =
@@ -78,8 +110,8 @@ addDeckButton deckId =
]
deckList : List Game.DeckInfo -> List String -> Input.Model InputId Message -> Html Message
deckList decks loadingDecks deckId =
deckList : Bool -> List Game.DeckInfo -> List String -> Input.Model InputId Message -> Html Message
deckList canNotChangeConfig decks loadingDecks deckId =
table [ class "decks mui-table" ]
[ thead []
[ tr []
@@ -90,10 +122,14 @@ deckList decks loadingDecks deckId =
]
]
, Util.tbody [] (List.concat
[ emptyDeckListInfo ((List.isEmpty decks) && List.isEmpty loadingDecks)
[ if (canNotChangeConfig && (List.isEmpty decks) ) then
[ ("!!emptyInfo", tr [] [ td [ colspan 4 ] [ text "No decks have been added yet." ] ]) ]
else
[]
, emptyDeckListInfo ((not canNotChangeConfig) && (List.isEmpty decks) && List.isEmpty loadingDecks)
, List.map loadedDeckEntry decks
, List.map loadingDeckEntry loadingDecks
, [ ("!!input", tr [] [ td [ colspan 4 ] [ Input.view deckId ] ]) ]
, if (canNotChangeConfig) then [] else [ ("!!input", tr [] [ td [ colspan 4 ] [ Input.view deckId ] ]) ]
])
]
@@ -140,8 +176,8 @@ emptyDeckListInfo display =
[]
houseRule : Bool -> HouseRule -> Html Message
houseRule enabled rule =
houseRule : Bool -> Bool -> HouseRule -> Html Message
houseRule canNotChangeConfig enabled rule =
let
(buttonText, command) =
if enabled then
@@ -149,22 +185,22 @@ houseRule enabled rule =
else
("Enable", EnableRule rule.id)
in
houseRuleTemplate (HouseRule.toString rule.id) rule.name rule.icon rule.description buttonText command
houseRuleTemplate canNotChangeConfig (HouseRule.toString rule.id) rule.name rule.icon rule.description buttonText command
houseRuleTemplate : String -> String -> String -> String -> String -> msg -> Html msg
houseRuleTemplate id' title icon description buttonText message =
houseRuleTemplate : Bool -> String -> String -> String -> String -> String -> msg -> Html msg
houseRuleTemplate canNotChangeConfig id' title icon description buttonText message =
div [ id id', class "house-rule" ]
[ div [] [ h3 [] [ Icon.icon icon, text " ", text title ]
, button [ class "mui-btn mui-btn--small mui-btn--primary", onClick message ]
, button [ class "mui-btn mui-btn--small mui-btn--primary", onClick message, disabled canNotChangeConfig ]
[ text buttonText ]
]
, p [] [ text description ]
]
rando : Html Message
rando = houseRuleTemplate "rando" "Rando Cardrissian" "cogs"
rando : Bool -> Html Message
rando canNotChangeConfig = houseRuleTemplate canNotChangeConfig "rando" "Rando Cardrissian" "cogs"
"Every round, one random card will be played for an imaginary player named Rando Cardrissian, if he wins, all players go home in a state of everlasting shame."
"Add an AI player" AddAi
@@ -174,12 +210,12 @@ startGameWarning canStart = if canStart then text "" else
span [] [ Icon.icon "info-circle", text " You will need at least two players to start the game." ]
startGameButton : Bool -> Bool -> Html Message
startGameButton enoughPlayers enoughCards = div [ id "start-game" ]
startGameButton : Bool -> Bool -> Bool -> Html Message
startGameButton notOwner enoughPlayers enoughCards = div [ id "start-game" ]
[ startGameWarning enoughPlayers
, button
[ class "mui-btn mui-btn--primary mui-btn--raised"
, onClick StartGame
, disabled (not (enoughPlayers && enoughCards))
, disabled ((not (enoughPlayers && enoughCards)) || notOwner)
] [ text "Start Game" ]
]
+15 -11
View File
@@ -46,7 +46,7 @@ view model =
root model.sidebar.hidden
[ appHeader header model
, spacer
, scores model.sidebar.shownAsOverlay players
, scores model.secret.id model.lobby.owner model.sidebar.shownAsOverlay players
, contentWrapper contents
]
@@ -63,8 +63,8 @@ spacer : Html msg
spacer = div [ class "mui--appbar-height" ] []
scores : Bool -> List Player -> Html ConsumerMessage
scores shownAsOverlay players =
scores : Player.Id -> Player.Id -> Bool -> List Player -> Html ConsumerMessage
scores client owner shownAsOverlay players =
let
hideMessage = LocalMessage (SidebarMessage Sidebar.Hide)
closeLink = if shownAsOverlay then
@@ -84,10 +84,10 @@ scores shownAsOverlay players =
, table [ class "mui-table" ]
[ thead [] [ tr [] [ th [ class "state", title "State" ] [ Icon.icon "tasks" ]
, th [ class "name" ] [ text "Player" ]
, th [ class "score", title "Score" ] [ Icon.icon "star" ]
, th [ class "score", title "Score" ] [ Icon.icon "trophy" ]
]
]
, tbody [] (List.map score players)
, tbody [] (List.map (score client owner) players)
]
]
in
@@ -102,19 +102,23 @@ scores shownAsOverlay players =
sidebar
score : Player -> Html msg
score player =
score : Player.Id -> Player.Id -> Player -> Html msg
score client owner player =
let
classes = classList
[ (Player.statusName player.status, True)
, ("disconnected", player.disconnected)
, ("left", player.left)
]
prename = if player.disconnected then [ Icon.icon "minus-circle", text " " ] else []
prename = ((if player.id == owner then [ Icon.icon "star", text " " ] else [])
++ (if player.disconnected then [ Icon.icon "minus-circle", text " " ] else []))
afterNameTitle = (if player.id == owner then " (Owner)" else "")
++ (if player.id == client then " (You)" else "")
++ (if player.disconnected then " (Disconnected)" else "")
in
tr [ classes ]
[ td [ class "state", title (statusDescription player) ] [ (playerIcon player) ]
, td [ class "name", title player.name ] (List.append prename [ text player.name ])
, td [ class "name", title (player.name ++ afterNameTitle) ] (List.append prename [ text player.name ])
, td [ class "score" ] [ text (toString player.score) ]
]
@@ -154,13 +158,13 @@ notificationPopup notification =
statusDescription : Player -> String
statusDescription player = (case player.status of
statusDescription player = case player.status of
NotPlayed -> "Choosing"
Played -> "Played"
Czar -> "Round Czar"
Ai -> "A Computer"
Neutral -> ""
Skipping -> "Being Skipped") ++ if player.disconnected then " (Disconnected)" else ""
Skipping -> "Being Skipped"
statusIcon : Status -> Html msg
+5 -1
View File
@@ -136,7 +136,11 @@ update message model =
(model, cmd)
CreateLobby ->
({ model | buttonsEnabled = False }, Request.send' API.createLobby ErrorMessage (\lobby -> JoinGivenLobbyAsNewPlayer lobby.gameCode))
( { model | buttonsEnabled = False }
, Request.send'
(API.createLobby model.nameInput.value)
ErrorMessage
(\gameCodeAndSecret -> StoreCredentialsAndMoveToLobby gameCodeAndSecret.gameCode gameCodeAndSecret.secret))
SubmitCurrentTab ->
case model.tabs.current of