Resolve #100: Likes now actually have an effect.

This commit is contained in:
Gareth Latty
2020-01-19 23:33:37 +00:00
parent 7702981ddb
commit 3a1189a912
35 changed files with 330 additions and 113 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ bunch of people in the same room on phones, or on voice chat online.
**[Play Massive Decks][hosted]**
[hosted]: http://md.rereadgames.com/
[hosted]: https://md.rereadgames.com/
[cah]: https://cardsagainsthumanity.com/
## About
+20 -1
View File
@@ -1,12 +1,15 @@
module MassiveDecks.Card.Play exposing
( Id
( Details
, Id
, Known
, Play
, WithLikes
, asKnown
, asPlay
)
import MassiveDecks.Card.Model as Card
import MassiveDecks.User as User
{-| The id for a play.
@@ -23,6 +26,22 @@ type alias Play =
}
{-| Details we find out about a play when the round is complete.
-}
type alias Details =
{ playedBy : User.Id
, likes : Maybe Int
}
{-| A play with the number of likes it received.
-}
type alias WithLikes =
{ play : List Card.Response
, likes : Maybe Int
}
{-| A known play.
-}
type alias Known =
+44 -14
View File
@@ -177,23 +177,24 @@ update wrap shared msg model =
( model, cmd )
Like ->
-- TODO: Actually sending this somewhere.
let
newRound =
( newRound, action ) =
case game.round of
Round.J judging ->
{ judging
( { judging
| liked =
judging.pick
|> Maybe.map (\p -> Set.insert p judging.liked)
|> Maybe.withDefault judging.liked
}
}
|> Round.J
, judging.pick |> Maybe.map Actions.like |> Maybe.withDefault Cmd.none
)
_ ->
game.round
( game.round, Cmd.none )
in
( { model | game = { game | round = newRound } }, Cmd.none )
( { model | game = { game | round = newRound } }, action )
SetPlayStyles playStyles ->
( { model | playStyles = playStyles }, Cmd.none )
@@ -413,10 +414,10 @@ applyGameEvent wrap wrapEvent auth shared gameEvent model =
game =
model.game
newPlayers =
playersWithWinner =
game.players |> Dict.update winner (Maybe.map (\p -> { p | score = p.score + 1 }))
( newRound, history, speech ) =
{ newRound, history, speech, newPlayers } =
case game.round of
Round.J r ->
let
@@ -439,16 +440,36 @@ applyGameEvent wrap wrapEvent auth shared gameEvent model =
tts =
Dict.get winner playsDict
|> Maybe.map (\p -> speak shared r.call (Just p))
|> Maybe.map (\p -> speak shared r.call (Just p.play))
|> Maybe.withDefault Cmd.none
ps =
playersWithWinner |> Dict.map (updateLikes playsDict)
in
( complete |> Round.C, complete :: game.history, tts )
{ newRound = complete |> Round.C
, history = complete :: game.history
, speech = tts
, newPlayers = ps
}
_ ->
-- TODO: Error
( game.round, game.history, Cmd.none )
{ newRound = game.round
, history = game.history
, speech = Cmd.none
, newPlayers = playersWithWinner
}
in
( { model | game = { game | round = newRound, players = newPlayers, history = history } }, speech )
( { model
| game =
{ game
| round = newRound
, players = newPlayers
, history = history
}
}
, speech
)
Events.RoundStarted { id, czar, players, call, drawn } ->
let
@@ -629,6 +650,15 @@ hotJoinPlayer player model =
{- Private -}
updateLikes : Dict User.Id Play.WithLikes -> User.Id -> Player -> Player
updateLikes plays uid player =
plays
|> Dict.get uid
|> Maybe.andThen .likes
|> Maybe.map (\likes -> { player | likes = likes + player.likes })
|> Maybe.withDefault player
viewWinner : Shared -> Dict User.Id User -> Set User.Id -> List (Html msg)
viewWinner shared users winners =
[ Wl.card [ HtmlA.id "game-winner" ]
@@ -911,9 +941,9 @@ reveal target responses play =
play
resolvePlayedBy : Dict Play.Id User.Id -> Play.Known -> Maybe ( User.Id, List Card.Response )
resolvePlayedBy : Dict Play.Id Play.Details -> Play.Known -> Maybe ( User.Id, Play.WithLikes )
resolvePlayedBy playedBy k =
Dict.get k.id playedBy |> Maybe.map (\u -> ( u, k.responses ))
Dict.get k.id playedBy |> Maybe.map (\d -> ( d.playedBy, { play = k.responses, likes = d.likes } ))
type alias ActionDetails msg =
@@ -8,9 +8,7 @@ import Html.Events as HtmlE
import MassiveDecks.Components as Components
import MassiveDecks.Game.Action.Model exposing (..)
import MassiveDecks.Game.Messages as Game exposing (Msg)
import MassiveDecks.Messages as Global
import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.Messages as Lobby
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Strings.Languages as Lang
import Weightless.Attributes as WlA
+11 -7
View File
@@ -8,6 +8,7 @@ import Html.Events as HtmlE
import Html.Keyed as HtmlK
import MassiveDecks.Card.Call as Call
import MassiveDecks.Card.Model as Card
import MassiveDecks.Card.Play as Play
import MassiveDecks.Card.Response as Response
import MassiveDecks.Components as Components
import MassiveDecks.Game.Messages exposing (Msg(..))
@@ -54,13 +55,16 @@ viewRound : Shared -> Config -> Dict User.Id User -> Round.Complete -> ( String,
viewRound shared config users round =
let
winning =
round.plays |> Dict.get round.winner |> Maybe.withDefault [] |> List.map .body
round.plays |> Dict.get round.winner
winningBody =
winning |> Maybe.map .play |> Maybe.withDefault [] |> List.map .body
in
( Round.idString round.id
, Html.li [ HtmlA.class "historic-round" ]
[ Html.div [ HtmlA.class "historic-call with-byline" ]
[ Plays.byLine shared users round.czar (Just Icon.gavel)
, Html.div [] [ Call.viewFilled shared config Card.Front [] winning round.call ]
[ Plays.byLine shared users round.czar (Just Icon.gavel) Nothing
, Html.div [] [ Call.viewFilled shared config Card.Front [] winningBody round.call ]
]
, HtmlK.ul [ HtmlA.class "plays cards" ]
(round.plays |> Dict.toList |> List.map (viewPlay shared config users round.winner))
@@ -68,15 +72,15 @@ viewRound shared config users round =
)
viewPlay : Shared -> Config -> Dict User.Id User -> User.Id -> ( User.Id, List Card.Response ) -> ( String, Html msg )
viewPlay shared config users winner ( id, responses ) =
viewPlay : Shared -> Config -> Dict User.Id User -> User.Id -> ( User.Id, Play.WithLikes ) -> ( String, Html msg )
viewPlay shared config users winner ( id, { play, likes } ) =
let
cards =
responses |> List.map (\r -> ( r.details.id, Html.li [] [ Response.view config Card.Front [] r ] ))
play |> List.map (\r -> ( r.details.id, Html.li [] [ Response.view config Card.Front [] r ] ))
in
( id
, Html.li [ HtmlA.class "with-byline" ]
[ Plays.byLine shared users id (Icon.trophy |> Maybe.justIf (winner == id))
[ Plays.byLine shared users id (Icon.trophy |> Maybe.justIf (winner == id)) likes
, HtmlK.ol [ HtmlA.class "play card-set" ] cards
]
)
@@ -21,6 +21,7 @@ import Set
-}
type alias Player =
{ score : Score
, likes : Int
, presence : Presence
}
@@ -28,6 +29,7 @@ type alias Player =
default : Player
default =
{ score = 0
, likes = 0
, presence = Active
}
+2 -2
View File
@@ -179,10 +179,10 @@ judging id czar players call plays startedAt timedOut =
{-| A round that has been finished.
-}
type alias Complete =
Data { plays : Dict User.Id (List Card.Response), playOrder : List User.Id, winner : User.Id }
Data { plays : Dict User.Id Play.WithLikes, playOrder : List User.Id, winner : User.Id }
complete : Id -> User.Id -> Set User.Id -> Card.Call -> Dict User.Id (List Card.Response) -> List User.Id -> User.Id -> Time -> Complete
complete : Id -> User.Id -> Set User.Id -> Card.Call -> Dict User.Id Play.WithLikes -> List User.Id -> User.Id -> Time -> Complete
complete id czar players call plays playOrder winner startedAt =
{ id = id
, czar = czar
@@ -6,6 +6,7 @@ import Html exposing (Html)
import Html.Attributes as HtmlA
import Html.Keyed as HtmlK
import MassiveDecks.Card.Model as Card
import MassiveDecks.Card.Play as Play
import MassiveDecks.Card.Response as Response
import MassiveDecks.Game.Action.Model as Action
import MassiveDecks.Game.Model exposing (..)
@@ -29,22 +30,22 @@ view shared nextRoundReady config users round =
, content =
HtmlK.ul [ HtmlA.class "complete plays cards" ]
(round.playOrder
|> List.map (\u -> ( u, Dict.get u round.plays |> Maybe.withDefault [] ))
|> List.map (\u -> ( u, Dict.get u round.plays ))
|> List.map (viewPlay shared config users round.winner)
)
, fillCallWith = winning |> Maybe.withDefault []
, fillCallWith = winning |> Maybe.map .play |> Maybe.withDefault []
}
viewPlay : Shared -> Config -> Dict User.Id User -> User.Id -> ( User.Id, List Card.Response ) -> ( String, Html msg )
viewPlay shared config users winner ( id, responses ) =
viewPlay : Shared -> Config -> Dict User.Id User -> User.Id -> ( User.Id, Maybe Play.WithLikes ) -> ( String, Html msg )
viewPlay shared config users winner ( id, play ) =
let
cards =
responses |> List.map (\r -> ( r.details.id, Html.li [] [ Response.view config Card.Front [] r ] ))
play |> Maybe.map (.play >> List.map (\r -> ( r.details.id, Html.li [] [ Response.view config Card.Front [] r ] ))) |> Maybe.withDefault []
in
( id
, Html.li [ HtmlA.class "with-byline" ]
[ Plays.byLine shared users id (Icon.trophy |> Maybe.justIf (winner == id))
[ Plays.byLine shared users id (Icon.trophy |> Maybe.justIf (winner == id)) (play |> Maybe.andThen .likes)
, HtmlK.ol [ HtmlA.class "play card-set" ] cards
]
)
@@ -34,8 +34,8 @@ view class picked details =
{-| Create a byline.
-}
byLine : Shared -> Dict User.Id User -> User.Id -> Maybe Icon -> Html msg
byLine shared users id icon =
byLine : Shared -> Dict User.Id User -> User.Id -> Maybe Icon -> Maybe Int -> Html msg
byLine shared users id icon likes =
let
name =
users |> Dict.get id |> Maybe.map .name |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared)
@@ -43,6 +43,7 @@ byLine shared users id icon =
Html.span [ HtmlA.class "byline", HtmlA.title name ]
[ icon |> Maybe.map Icon.viewIcon |> Maybe.withDefault Html.nothing
, Html.text name
, likes |> Maybe.map (\l -> Strings.Likes { total = l } |> Lang.html shared) |> Maybe.withDefault Html.nothing
]
@@ -310,8 +310,9 @@ rando =
player : Json.Decoder Player
player =
Json.map2 Player
Json.map3 Player
(Json.field "score" score)
(Json.field "likes" Json.int)
(Json.field "presence" playerPresence)
@@ -621,7 +622,14 @@ roundFinished : Json.Decoder Events.TimedGameEvent
roundFinished =
Json.map2 (\wi -> \pb -> Events.RoundFinished { winner = wi, playedBy = pb })
(Json.field "winner" userId)
(Json.field "playedBy" (Json.dict userId))
(Json.field "playDetails" (Json.dict playDetails))
playDetails : Json.Decoder Play.Details
playDetails =
Json.map2 Play.Details
(Json.field "playedBy" userId)
(Json.maybe (Json.field "likes" Json.int))
playRevealed : Json.Decoder Events.TimedGameEvent
@@ -1026,12 +1034,19 @@ completeRound =
(Json.field "czar" userId)
(Json.field "players" playerSet)
(Json.field "call" call)
(Json.field "plays" (Json.dict (Json.list response)))
(Json.field "plays" (Json.dict playWithLikes))
(Json.field "playOrder" (Json.list userId))
(Json.field "winner" userId)
(Json.field "startedAt" Time.timeDecoder)
playWithLikes : Json.Decoder Play.WithLikes
playWithLikes =
Json.map2 Play.WithLikes
(Json.field "play" (Json.list response))
(Json.maybe (Json.field "likes" Json.int))
playerRole : Json.Decoder Player.Role
playerRole =
Json.string |> Json.andThen playerRoleByName
+4 -1
View File
@@ -772,8 +772,11 @@ userDetails shared game userId user =
score =
player |> Maybe.map (\p -> Strings.Score { total = p.score })
likes =
player |> Maybe.map (\p -> Strings.Likes { total = p.likes })
in
( viewDetails shared details, viewDetails shared [ score ] )
( viewDetails shared details, viewDetails shared [ score, likes ] )
playStateDetail : Maybe Round -> User.Id -> Maybe MdString
@@ -8,6 +8,7 @@ module MassiveDecks.Pages.Lobby.Actions exposing
, judge
, kick
, leave
, like
, redraw
, removeDeck
, reveal
@@ -95,6 +96,11 @@ judge play =
action "Judge" [ ( "winner", play |> Json.string ) ]
like : Play.Id -> Cmd msg
like play =
action "Like" [ ( "play", play |> Json.string ) ]
redraw : Cmd msg
redraw =
action "Redraw" []
@@ -90,5 +90,5 @@ type TimedGameEvent
, drawn : Maybe (List Card.Response)
}
| StartRevealing { plays : List Play.Id, drawn : Maybe (List Card.Response) }
| RoundFinished { winner : User.Id, playedBy : Dict Play.Id User.Id }
| RoundFinished { winner : User.Id, playedBy : Dict Play.Id Play.Details }
| PlayRevealed { id : Play.Id, play : List Card.Response }
@@ -7,12 +7,14 @@ import Html exposing (Html)
import Html.Attributes as HtmlA
import MassiveDecks.Card.Call as Call
import MassiveDecks.Card.Model as Card exposing (Call)
import MassiveDecks.Card.Play exposing (Play)
import MassiveDecks.Card.Play as Play exposing (Play)
import MassiveDecks.Card.Response as Response
import MassiveDecks.Game.Model as Game
import MassiveDecks.Game.Round as Round exposing (Round)
import MassiveDecks.Model exposing (Shared)
import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config)
import MassiveDecks.Strings as Strings
import MassiveDecks.Strings.Languages as Lang
import MassiveDecks.User as User exposing (User)
import MassiveDecks.Util.Maybe as Maybe
import Round
@@ -62,10 +64,10 @@ viewRevealing shared config users round =
Nothing
plays =
round.plays |> List.map (\p -> ( Nothing, p.responses ))
round.plays |> List.map (\p -> ( Nothing, p.responses |> Maybe.map (\r -> { play = r, likes = Nothing }) ))
in
[ viewCall shared config fillWith round.call
, viewPlays config (round.call |> Call.slotCount) users Nothing plays
, viewPlays shared config (round.call |> Call.slotCount) users Nothing plays
]
@@ -73,10 +75,10 @@ viewJudging : Shared -> Config -> Dict User.Id User -> Round.Judging -> List (Ht
viewJudging shared config users round =
let
plays =
round.plays |> List.map (\p -> ( Nothing, Just p.responses ))
round.plays |> List.map (\p -> ( Nothing, Just { play = p.responses, likes = Nothing } ))
in
[ viewCall shared config Nothing round.call
, viewPlays config (round.call |> Call.slotCount) users Nothing plays
, viewPlays shared config (round.call |> Call.slotCount) users Nothing plays
]
@@ -87,10 +89,10 @@ viewComplete shared config users round =
round.playOrder |> List.map (\u -> ( Just u, Dict.get u round.plays ))
winner =
Dict.get round.winner round.plays
Dict.get round.winner round.plays |> Maybe.map .play
in
[ viewCall shared config winner round.call
, viewPlays config (round.call |> Call.slotCount) users (Just round.winner) plays
, viewPlays shared config (round.call |> Call.slotCount) users (Just round.winner) plays
]
@@ -117,17 +119,17 @@ viewUnknownPlays slotCount playStyles players played =
)
viewPlays : Config -> Int -> Dict User.Id User -> Maybe User.Id -> List ( Maybe User.Id, Maybe (List Card.Response) ) -> Html msg
viewPlays config slotCount users winner plays =
viewPlays : Shared -> Config -> Int -> Dict User.Id User -> Maybe User.Id -> List ( Maybe User.Id, Maybe Play.WithLikes ) -> Html msg
viewPlays shared config slotCount users winner plays =
let
angle =
plays |> List.length |> anglePerPlay
in
Html.ul [ HtmlA.id "plays" ] (plays |> List.indexedMap (viewPlayByIndex config slotCount users winner angle))
Html.ul [ HtmlA.id "plays" ] (plays |> List.indexedMap (viewPlayByIndex shared config slotCount users winner angle))
viewPlayByIndex : Config -> Int -> Dict User.Id User -> Maybe User.Id -> Float -> Int -> ( Maybe User.Id, Maybe (List Card.Response) ) -> Html msg
viewPlayByIndex config slotCount users winner angle index ( user, play ) =
viewPlayByIndex : Shared -> Config -> Int -> Dict User.Id User -> Maybe User.Id -> Float -> Int -> ( Maybe User.Id, Maybe Play.WithLikes ) -> Html msg
viewPlayByIndex shared config slotCount users winner angle index ( user, play ) =
let
isWinner =
case winner of
@@ -137,7 +139,7 @@ viewPlayByIndex config slotCount users winner angle index ( user, play ) =
Nothing ->
False
in
viewPlay config slotCount (toFloat index * angle) (user |> Maybe.andThen (\u -> Dict.get u users)) isWinner play
viewPlay shared config slotCount (toFloat index * angle) (user |> Maybe.andThen (\u -> Dict.get u users)) isWinner play
anglePerPlay : Int -> Float
@@ -152,11 +154,11 @@ closeDistance =
farDistance : number
farDistance =
85
150
viewPlay : Config -> Int -> Float -> Maybe User -> Bool -> Maybe (List Card.Response) -> Html msg
viewPlay config slotCount angle playedBy isWinner responses =
viewPlay : Shared -> Config -> Int -> Float -> Maybe User -> Bool -> Maybe Play.WithLikes -> Html msg
viewPlay shared config slotCount angle playedBy isWinner play =
Html.li
(positionFromAngle angle closeDistance)
[ Html.div [ HtmlA.class "with-byline" ]
@@ -164,13 +166,14 @@ viewPlay config slotCount angle playedBy isWinner responses =
(List.filterMap identity
[ Icon.viewIcon Icon.trophy |> Maybe.justIf isWinner
, playedBy |> Maybe.map (.name >> Html.text)
, play |> Maybe.andThen .likes |> Maybe.map (\l -> Strings.Likes { total = l } |> Lang.html shared)
]
)
, Html.ol
[ HtmlA.classList [ ( "play", True ), ( "card-set", True ) ]
]
(responses
|> Maybe.map (List.map (\response -> Html.li [] [ Response.view config Card.Front [] response ]))
(play
|> Maybe.map (.play >> List.map (\response -> Html.li [] [ Response.view config Card.Front [] response ]))
|> Maybe.withDefault (List.repeat slotCount (Html.li [] [ Response.viewUnknown [] ]))
)
]
+2
View File
@@ -141,6 +141,8 @@ type MdString
| AiDescription -- A description of a player who is controlled by the computer.
| Score { total : Int } -- A display of a score.
| ScoreDescription -- A description of a player's score.
| Likes { total : Int } -- A display of a number of likes.
| LikesDescription -- A description of the number of likes a play received or a player has recieved.
| ToggleUserList -- A description of the action of showing or hiding the user list.
| GameMenu -- A description of the game menu.
| UnknownUser -- A name for a user that doesn't have a known name.
@@ -503,6 +503,13 @@ translate mdString =
, Text " this player has."
]
Likes { total } ->
[ Text (String.fromInt total) ]
LikesDescription ->
[ Text "The number of likes received."
]
ToggleUserList ->
[ Text "Show or hide the scoreboard." ]
@@ -156,6 +156,9 @@ enhanceHtml context mdString unenhanced =
Score _ ->
[ Html.span [ HtmlA.class "no-wrap", (ScoreDescription |> asString context) |> HtmlA.title ] (suffixed unenhanced Icon.star) ]
Likes _ ->
[ Html.span [ HtmlA.class "no-wrap", (LikesDescription |> asString context) |> HtmlA.title ] (suffixed unenhanced Icon.thumbsUp) ]
NumberOfCards _ ->
[ Html.span [ HtmlA.class "amount" ] unenhanced ]
+2 -2
View File
@@ -16,7 +16,7 @@
max-width: 100%;
overflow: hidden;
svg {
margin-right: 0.5em;
> * {
margin: 0 0.25em 0 0.25em;
}
}
+1
View File
@@ -108,6 +108,7 @@
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: $users-width - 8rem;
}
ol {
+1 -1
View File
@@ -1,2 +1,2 @@
$top-bar-height: 4rem;
$users-width: 15rem;
$users-width: 18rem;
+1 -1
View File
@@ -278,7 +278,7 @@ module.exports = (env, argv) => {
},
devServer: {
hot: true,
allowedHosts: ["localhost", "f2a7ec0b.ngrok.io"],
allowedHosts: ["localhost"],
proxy: {
// Forward to the server.
"/api/**": {
+13 -4
View File
@@ -26,13 +26,22 @@ export const handle: gameAction.Handler<Judge> = (auth, lobby, action) => {
const lobbyRound = game.round;
const plays = lobbyRound.plays;
if (lobbyRound.verifyStage<round.Judging>(action, "Judging")) {
const play = plays.find(play => play.id === action.winner);
if (play === undefined) {
let winningPlay = undefined;
for (const play of plays) {
if (play.likes.size > 0) {
const player = game.players.get(play.playedBy) as Player;
player.likes += play.likes.size;
}
if (play.id === action.winner) {
winningPlay = play;
}
}
if (winningPlay === undefined) {
throw new InvalidActionError("Given play doesn't exist.");
}
const player = game.players.get(play.playedBy) as Player;
const player = game.players.get(winningPlay.playedBy) as Player;
player.score += 1;
const completedRound = lobbyRound.advance(play.playedBy);
const completedRound = lobbyRound.advance(winningPlay.playedBy);
game.round = completedRound;
game.history.splice(0, 0, completedRound.public());
+6 -2
View File
@@ -2,6 +2,8 @@ import wu from "wu";
import { Action } from "../../action";
import * as util from "../../util";
import * as gameAction from "../game-action";
import { Like } from "./player/like";
import * as like from "./player/like";
import * as submit from "./player/submit";
import { Submit } from "./player/submit";
import * as takeBack from "./player/take-back";
@@ -10,9 +12,9 @@ import { TakeBack } from "./player/take-back";
/**
* An action only the czar can perform.
*/
export type Player = Submit | TakeBack;
export type Player = Submit | TakeBack | Like;
const possible = new Set([submit.is, takeBack.is]);
const possible = new Set([submit.is, takeBack.is, like.is]);
/**
* Check if an action is a non-czar action.
@@ -32,6 +34,8 @@ export const handle: gameAction.Handler<Player> = (
return submit.handle(auth, lobby, action, server);
} else if (takeBack.is(action)) {
return takeBack.handle(auth, lobby, action, server);
} else if (like.is(action)) {
return like.handle(auth, lobby, action, server);
} else {
return util.assertNever(action);
}
@@ -0,0 +1,47 @@
import { Action } from "../../../action";
import * as round from "../../../games/game/round";
import * as gameAction from "../../game-action";
import * as play from "../../../games/cards/play";
/**
* A player plays a white card into a round.
*/
export interface Like {
action: NameType;
play: play.Id;
}
type NameType = "Like";
const name: NameType = "Like";
/**
* Check if an action is a take back action.
* @param action The action to check.
*/
export const is = (action: Action): action is Like => action.action === name;
export const handle: gameAction.Handler<Like> = (auth, lobby, action) => {
if (
lobby.game.round.verifyStage<round.Revealing | round.Judging>(
action,
"Judging"
)
) {
const cRound = lobby.game.round;
const target = cRound.plays.find(p => p.id === action.play);
if (
target !== undefined &&
target.playedBy !== auth.uid &&
!target.likes.has(auth.uid)
) {
target.likes.add(auth.uid);
return {
lobby
};
} else {
return {};
}
} else {
return {};
}
};
@@ -14,7 +14,7 @@ import * as gameAction from "../../game-action";
* A player plays a white card into a round.
*/
export interface Submit {
action: "Submit";
action: NameType;
play: card.Id[];
}
@@ -67,7 +67,8 @@ export const handle: gameAction.Handler<Submit> = (
id: playId,
play: extractedPlay,
playedBy: auth.uid,
revealed: false
revealed: false,
likes: new Set()
});
const events = [event.targetAll(playSubmitted.of(auth.uid))];
const timeouts = [];
@@ -9,7 +9,7 @@ import * as gameAction from "../../game-action";
* A player plays a white card into a round.
*/
export interface TakeBack {
action: "TakeBack";
action: NameType;
}
type NameType = "TakeBack";
+52 -24
View File
@@ -34,6 +34,9 @@ export const Schema = {
{
$ref: "#/definitions/EnforceTimeLimit"
},
{
$ref: "#/definitions/Like"
},
{
$ref: "#/definitions/Submit"
},
@@ -96,7 +99,7 @@ export const Schema = {
description: "Authenticate with the game.",
properties: {
action: {
$ref: "#/definitions/NameType_11"
$ref: "#/definitions/NameType_14"
},
token: {
$ref: "#/definitions/Token"
@@ -161,7 +164,7 @@ export const Schema = {
description: "Make a change to the configuration of decks for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_2"
$ref: "#/definitions/NameType_5"
},
change: {
$ref: "#/definitions/PlayerDriven"
@@ -184,7 +187,7 @@ export const Schema = {
description: "Set the hand size for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_3"
$ref: "#/definitions/NameType_6"
},
change: {
$ref: "#/definitions/Change"
@@ -246,7 +249,7 @@ export const Schema = {
description: "Change the time limit for a given stage.",
properties: {
action: {
$ref: "#/definitions/NameType_4"
$ref: "#/definitions/NameType_7"
},
if: {
description:
@@ -270,7 +273,7 @@ export const Schema = {
"Change the time limit mode, or disable the time limits completely.",
properties: {
action: {
$ref: "#/definitions/NameType_4"
$ref: "#/definitions/NameType_7"
},
if: {
description:
@@ -323,7 +326,7 @@ export const Schema = {
description: "End the current game.",
properties: {
action: {
$ref: "#/definitions/NameType_9"
$ref: "#/definitions/NameType_12"
}
},
required: ["action"],
@@ -400,6 +403,21 @@ export const Schema = {
required: ["action"],
type: "object"
},
Like: {
additionalProperties: false,
defaultProperties: [],
description: "A player plays a white card into a round.",
properties: {
action: {
$ref: "#/definitions/NameType_2"
},
play: {
$ref: "#/definitions/Id"
}
},
required: ["action", "play"],
type: "object"
},
Name: {
description: "The name the user goes by.",
maxLength: 100,
@@ -415,43 +433,55 @@ export const Schema = {
type: "string"
},
NameType_10: {
enum: ["StartGame"],
enum: ["SetPublic"],
type: "string"
},
NameType_11: {
enum: ["SetScoreLimit"],
type: "string"
},
NameType_12: {
enum: ["EndGame"],
type: "string"
},
NameType_13: {
enum: ["StartGame"],
type: "string"
},
NameType_14: {
enum: ["Authenticate"],
type: "string"
},
NameType_2: {
enum: ["ChangeDecks"],
enum: ["Like"],
type: "string"
},
NameType_3: {
enum: ["ChangeHouseRule"],
enum: ["Submit"],
type: "string"
},
NameType_4: {
enum: ["ChangeTimeLimit"],
enum: ["TakeBack"],
type: "string"
},
NameType_5: {
enum: ["SetHandSize"],
enum: ["ChangeDecks"],
type: "string"
},
NameType_6: {
enum: ["SetPassword"],
enum: ["ChangeHouseRule"],
type: "string"
},
NameType_7: {
enum: ["SetPublic"],
enum: ["ChangeTimeLimit"],
type: "string"
},
NameType_8: {
enum: ["SetScoreLimit"],
enum: ["SetHandSize"],
type: "string"
},
NameType_9: {
enum: ["EndGame"],
enum: ["SetPassword"],
type: "string"
},
PackingHeat: {
@@ -562,7 +592,7 @@ export const Schema = {
description: "Set the hand size for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_5"
$ref: "#/definitions/NameType_8"
},
handSize: {
description: "The number of cards in each player's hand.",
@@ -584,7 +614,7 @@ export const Schema = {
description: "Set (or unset) the password for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_6"
$ref: "#/definitions/NameType_9"
},
if: {
description:
@@ -658,7 +688,7 @@ export const Schema = {
description: "Set (or unset) the password for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_7"
$ref: "#/definitions/NameType_10"
},
if: {
description:
@@ -678,7 +708,7 @@ export const Schema = {
description: "(Un)Set the score limit for the lobby.",
properties: {
action: {
$ref: "#/definitions/NameType_8"
$ref: "#/definitions/NameType_11"
},
if: {
description:
@@ -705,7 +735,7 @@ export const Schema = {
description: "Start a game in the lobby if possible.",
properties: {
action: {
$ref: "#/definitions/NameType_10"
$ref: "#/definitions/NameType_13"
}
},
required: ["action"],
@@ -717,8 +747,7 @@ export const Schema = {
description: "A player plays a white card into a round.",
properties: {
action: {
enum: ["Submit"],
type: "string"
$ref: "#/definitions/NameType_3"
},
play: {
items: {
@@ -736,8 +765,7 @@ export const Schema = {
description: "A player plays a white card into a round.",
properties: {
action: {
enum: ["TakeBack"],
type: "string"
$ref: "#/definitions/NameType_4"
}
},
required: ["action"],
@@ -115,18 +115,18 @@ export class IncorrectUserRoleError extends ActionExecutionError {
interface IncorrectRoundStageDetails extends errors.Details {
stage: round.Stage;
expected: round.Stage;
expected: round.Stage[];
}
// Could happen if the round changes unexpectedly (e.g: czar leaves game).
export class IncorrectRoundStageError extends ActionExecutionError {
public readonly stage: round.Stage;
public readonly expected: round.Stage;
public readonly expected: round.Stage[];
public constructor(
action: Action,
stage: round.Stage,
expected: round.Stage
...expected: round.Stage[]
) {
super(
`For this action the round must be in the ${expected} stage but is in ` +
@@ -1,4 +1,5 @@
import * as gameRound from "../../games/game/round";
import * as publicRound from "../../games/game/round/public";
import * as user from "../../user";
/**
@@ -8,11 +9,11 @@ import * as user from "../../user";
export interface RoundFinished {
event: "RoundFinished";
winner: user.Id;
playedBy: { [id: string]: user.Id };
playDetails: { [id: string]: publicRound.PlayDetails };
}
export const of = (round: gameRound.Complete): RoundFinished => ({
event: "RoundFinished",
winner: round.winner,
playedBy: round.playedBy()
playDetails: round.playDetails()
});
+1 -1
View File
@@ -21,7 +21,7 @@ export interface PotentiallyRevealed {
/**
* A play with its id.
*/
export type Revealed = PotentiallyRevealed & { play: Play };
export type Revealed = PotentiallyRevealed & { play: Play; likes?: number };
/**
* Create a new user id.
+2 -1
View File
@@ -92,7 +92,8 @@ export const atStartOfRound = (
id: playId,
play: player.hand.slice(0, slotCount),
playedBy: ai,
revealed: false
revealed: false,
likes: new Set()
});
events.push(event.targetAll(playSubmitted.of(ai)));
}
+17 -9
View File
@@ -2,14 +2,13 @@ import wu from "wu";
import { Action } from "../../action";
import { IncorrectRoundStageError } from "../../errors/action-execution-error";
import * as user from "../../user";
import * as util from "../../util";
import * as card from "../cards/card";
import * as play from "../cards/play";
import { Play } from "../cards/play";
import * as publicRound from "./round/public";
import { Public as PublicRound } from "./round/public";
import * as storedPlay from "./round/storedPlay";
import { StoredPlay } from "./round/storedPlay";
import * as util from "../../util";
export type Round = Playing | Revealing | Judging | Complete;
@@ -109,18 +108,24 @@ export class Complete extends Base<"Complete"> {
};
}
public playedBy(): { [player: string]: user.Id } {
const obj: { [player: string]: user.Id } = {};
public playDetails(): { [player: string]: publicRound.PlayDetails } {
const obj: { [player: string]: publicRound.PlayDetails } = {};
for (const roundPlay of this.plays) {
obj[roundPlay.id] = roundPlay.playedBy;
obj[roundPlay.id] = {
playedBy: roundPlay.playedBy,
...(roundPlay.likes.size > 0 ? { likes: roundPlay.likes.size } : {})
};
}
return obj;
}
private playsObj(): { [player: string]: Play } {
const obj: { [player: string]: Play } = {};
private playsObj(): { [player: string]: publicRound.PlayWithLikes } {
const obj: { [player: string]: publicRound.PlayWithLikes } = {};
for (const roundPlay of this.plays) {
obj[roundPlay.playedBy] = roundPlay.play;
obj[roundPlay.playedBy] = {
play: roundPlay.play,
...(roundPlay.likes.size > 0 ? { likes: roundPlay.likes.size } : {})
};
}
return obj;
}
@@ -178,7 +183,10 @@ export class Judging extends Base<"Judging"> implements Timed {
private *revealedPlays(): Iterable<play.Revealed> {
for (const roundPlay of this.plays) {
yield { id: roundPlay.id, play: roundPlay.play };
yield {
id: roundPlay.id,
play: roundPlay.play
};
}
}
}
+12 -1
View File
@@ -2,6 +2,7 @@ import * as user from "../../../user";
import * as card from "../../cards/card";
import { Play } from "../../cards/play";
import * as play from "../../cards/play";
import * as player from "../../player";
export type Public = Playing | Revealing | Judging | Complete;
@@ -33,9 +34,19 @@ export interface Judging extends Base, Timed {
plays: play.Revealed[];
}
export interface PlayWithLikes {
play: Play;
likes?: number;
}
export interface Complete extends Base {
stage: "Complete";
winner: user.Id;
plays: { [player: string]: Play };
plays: { [player: string]: PlayWithLikes };
playOrder: user.Id[];
}
export interface PlayDetails {
playedBy: user.Id;
likes?: player.Likes;
}
@@ -8,6 +8,7 @@ export interface StoredPlay {
playedBy: user.Id;
play: Play;
revealed: boolean;
likes: Set<user.Id>;
}
export type Unrevealed = StoredPlay & { revealed: false };
+12 -1
View File
@@ -7,6 +7,7 @@ import { Game } from "./game";
*/
export interface Public {
score: Score;
likes: Likes;
presence: Presence;
}
@@ -22,6 +23,13 @@ export type Role = "Czar" | "Player";
*/
export type Score = number;
/**
* How many likes the player has received.
* @TJS-type integer
* @minimum 0
*/
export type Likes = number;
/**
* If the player is active in the game or has been marked as away.
*/
@@ -34,17 +42,20 @@ export class Player {
public hand: Hand;
public score: Score;
public presence: Presence;
public likes: Likes;
public constructor(hand: Hand) {
this.hand = hand;
this.score = 0;
this.likes = 0;
this.presence = "Active";
}
public public(): Public {
return {
score: this.score,
presence: this.presence
presence: this.presence,
likes: this.likes
};
}