Add new deck source: JSON Against Humanity.

This commit is contained in:
Gareth Latty
2020-05-30 04:18:09 +01:00
parent e55267a06f
commit 9e50f08471
21 changed files with 649 additions and 21 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ init flags url key =
, speech = speech
, notifications = Notifications.init
, remoteMode = remoteMode
, sources = { builtIn = Nothing, manyDecks = Nothing }
, sources = { builtIn = Nothing, manyDecks = Nothing, jsonAgainstHumanity = Nothing }
}
( page, pageCmd ) =
+9 -1
View File
@@ -21,6 +21,7 @@ import Html.Attributes as HtmlA
import MassiveDecks.Card.Source.BuiltIn as BuiltIn
import MassiveDecks.Card.Source.Custom as Player
import MassiveDecks.Card.Source.Fake as Fake
import MassiveDecks.Card.Source.JsonAgainstHumanity as JsonAgainstHumanity
import MassiveDecks.Card.Source.ManyDecks as ManyDecks
import MassiveDecks.Card.Source.Methods exposing (..)
import MassiveDecks.Card.Source.Model exposing (..)
@@ -71,6 +72,9 @@ generalMethods source =
GManyDecks ->
ManyDecks.generalMethods
GJsonAgainstHumanity ->
JsonAgainstHumanity.generalMethods
{-| Get an empty source of the given type.
-}
@@ -180,12 +184,13 @@ generalEditor shared existing currentValue update submit noOp =
enabledSources =
[ shared.sources.builtIn |> Maybe.map (\_ -> BuiltIn.generalMethods)
, shared.sources.manyDecks |> Maybe.map (\_ -> ManyDecks.generalMethods)
, shared.sources.jsonAgainstHumanity |> Maybe.map (\_ -> JsonAgainstHumanity.generalMethods)
]
toItem source =
{ id = source.id ()
, icon = source.logo ()
, primary = [ () |> source.name |> Lang.html shared ]
, primary = [ () |> source.name |> Lang.string shared |> Html.text ]
, secondary = Nothing
, meta = Nothing
}
@@ -257,3 +262,6 @@ externalMethods external =
BuiltIn id ->
BuiltIn.methods id
JsonAgainstHumanity id ->
JsonAgainstHumanity.methods id
@@ -0,0 +1,154 @@
module MassiveDecks.Card.Source.JsonAgainstHumanity exposing
( generalMethods
, methods
)
import FontAwesome.Icon as Icon
import FontAwesome.Solid as Icon
import Html as Html exposing (Html)
import Html.Attributes as HtmlA
import List.Extra as List
import MassiveDecks.Card.Source.JsonAgainstHumanity.Model exposing (..)
import MassiveDecks.Card.Source.Methods as Source
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Components.Form.Message as Message exposing (Message)
import MassiveDecks.Model exposing (..)
import MassiveDecks.Pages.Lobby.Configure.Decks.Model exposing (DeckOrError)
import MassiveDecks.Strings as Strings exposing (MdString)
import MassiveDecks.Util.Html as Html
import MassiveDecks.Util.Maybe as Maybe
import Material.Select as Select
methods : Id -> Source.ExternalMethods msg
methods givenId =
{ name = name
, logo = logo
, empty = empty
, id = id
, messages = messages
, problems = problems givenId
, defaultDetails = details givenId
, tooltip = tooltip givenId
, editor = editor givenId
, equals = equals givenId
}
generalMethods : Source.ExternalGeneralMethods msg
generalMethods =
{ name = name
, logo = logo
, empty = empty
, id = id
, messages = messages
}
{- Private -}
id : () -> Source.General
id () =
Source.GJsonAgainstHumanity
name : () -> MdString
name () =
Strings.JsonAgainstHumanity
empty : Shared -> Source.External
empty shared =
shared.sources.jsonAgainstHumanity
|> Maybe.andThen (.decks >> List.head)
|> Maybe.map .id
|> Maybe.withDefault (hardcoded "")
|> Source.JsonAgainstHumanity
equals : Id -> Source.External -> Bool
equals givenId source =
case source of
Source.JsonAgainstHumanity other ->
givenId == other
_ ->
False
messages : () -> List (Message msg)
messages () =
[ Strings.JsonAgainstHumanityAbout |> Message.info ]
problems : Id -> () -> List (Message msg)
problems givenId () =
[]
editor : Id -> Shared -> List DeckOrError -> (Source.External -> msg) -> Maybe msg -> msg -> Html msg
editor selected shared existing update _ _ =
case shared.sources.jsonAgainstHumanity of
Just { decks } ->
let
deck d =
let
matches other =
case other.source of
Source.JsonAgainstHumanity o ->
o == d.id
_ ->
False
in
{ id = d.id
, icon = Nothing
, primary = [ Html.text d.name ]
, secondary = Nothing
, meta = Icon.check |> Icon.viewIcon |> Maybe.justIf (existing |> List.any matches)
}
in
Html.span [ HtmlA.id "json-against-humanity-editor", HtmlA.class "primary" ]
[ Select.view shared
{ label = Strings.Deck
, idToString = toString
, idFromString = fromString shared.sources.jsonAgainstHumanity
, selected = Just selected
, wrap = Maybe.withDefault (hardcoded "") >> Source.JsonAgainstHumanity >> update
}
[ HtmlA.id "built-in-selector" ]
(decks |> List.map deck)
]
Nothing ->
Html.nothing
details : Id -> Shared -> Source.Details
details givenId shared =
{ name =
shared.sources.jsonAgainstHumanity
|> Maybe.andThen (.decks >> List.find (\d -> d.id == givenId))
|> Maybe.map .name
|> Maybe.withDefault (givenId |> toString)
, url = Nothing
, author = Nothing
, translator = Nothing
, language = Nothing
}
tooltip : Id -> (String -> List (Html msg) -> Html msg) -> Maybe ( String, Html msg )
tooltip givenId tooltipRender =
let
forId =
"json-against-humanity-" ++ toString givenId
in
( forId, [] |> tooltipRender forId ) |> Just
logo : () -> Maybe (Html msg)
logo () =
Icon.code |> Icon.viewIcon |> Just
@@ -0,0 +1,73 @@
module MassiveDecks.Card.Source.JsonAgainstHumanity.Model exposing
( Deck
, Id
, Info
, fromString
, hardcoded
, idDecoder
, toString
)
{-| The id for a built-in deck.
-}
import Json.Decode as Json
import List.Extra as List
type Id
= Id String
{-| Information about the source from the server.
-}
type alias Info =
{ aboutUrl : String
, decks : List Deck
}
{-| Information about a deck.
-}
type alias Deck =
{ id : Id
, name : String
}
{-| Get an id from a string.
-}
fromString : Maybe Info -> String -> Maybe Id
fromString builtInInfo stringId =
let
matching got deck =
case deck.id of
Id str ->
str == got
internal { decks } =
decks |> List.find (matching stringId) |> Maybe.map .id
in
builtInInfo |> Maybe.andThen internal
{-| Allows you to hard-code an Id for a deck. This means that the client and server could end up out of sync if the id
doesn't actually exist.
-}
hardcoded : String -> Id
hardcoded =
Id
{-| A decoder for ids.
-}
idDecoder : Json.Decoder Id
idDecoder =
Json.string |> Json.map Id
{-| Convert an Id to it's string representation.
-}
toString : Id -> String
toString (Id str) =
str
@@ -13,6 +13,7 @@ module MassiveDecks.Card.Source.Model exposing
import Json.Decode as Json
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.JsonAgainstHumanity.Model as JsonAgainstHumanity
import MassiveDecks.Card.Source.ManyDecks.Model as ManyDecks
import MassiveDecks.Strings.Languages.Model exposing (Language)
@@ -22,6 +23,7 @@ import MassiveDecks.Strings.Languages.Model exposing (Language)
type General
= GBuiltIn
| GManyDecks
| GJsonAgainstHumanity
{-| Details on where game data came from.
@@ -46,6 +48,7 @@ sources are more limited and specific.
type External
= BuiltIn BuiltIn.Id
| ManyDecks ManyDecks.DeckCode
| JsonAgainstHumanity JsonAgainstHumanity.Id
{-| A summary of the contents of the source deck.
@@ -80,6 +83,7 @@ type LoadFailureReason
type alias Info =
{ builtIn : Maybe BuiltIn.Info
, manyDecks : Maybe ManyDecks.Info
, jsonAgainstHumanity : Maybe JsonAgainstHumanity.Info
}
@@ -94,6 +98,9 @@ generalToString source =
GManyDecks ->
"ManyDecks"
GJsonAgainstHumanity ->
"JAH"
{-| Get a general source by a string name.
-}
@@ -106,6 +113,9 @@ generalFromString sourceName =
"ManyDecks" ->
Just GManyDecks
"JAH" ->
Just GJsonAgainstHumanity
_ ->
Nothing
@@ -31,6 +31,7 @@ import MassiveDecks.Card.Model as Card exposing (Call, Response)
import MassiveDecks.Card.Parts as Parts exposing (Part, Parts)
import MassiveDecks.Card.Play as Play exposing (Play)
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.JsonAgainstHumanity.Model as JsonAgianstHumanity
import MassiveDecks.Card.Source.ManyDecks.Model as ManyDecks
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Cast.Model as Cast
@@ -71,6 +72,7 @@ sourceInfo =
Json.succeed Source.Info
|> Json.optional "builtIn" (builtInInfo |> Json.map Just) Nothing
|> Json.optional "manyDecks" (manyDecksInfo |> Json.map Just) Nothing
|> Json.optional "jsonAgainstHumanity" (jsonAgainstHumanityInfo |> Json.map Just) Nothing
|> Json.andThen atLeastOne
@@ -86,6 +88,19 @@ manyDecksInfo =
|> Json.required "baseUrl" Json.string
jsonAgainstHumanityInfo : Json.Decoder JsonAgianstHumanity.Info
jsonAgainstHumanityInfo =
let
jsonAgainstHumanityDeck =
Json.succeed JsonAgianstHumanity.Deck
|> Json.required "id" JsonAgianstHumanity.idDecoder
|> Json.required "name" Json.string
in
Json.succeed JsonAgianstHumanity.Info
|> Json.required "aboutUrl" Json.string
|> Json.required "decks" (Json.list jsonAgainstHumanityDeck)
builtInDeck : Json.Decoder BuiltIn.Deck
builtInDeck =
Json.succeed BuiltIn.Deck
@@ -230,6 +245,9 @@ externalSourceByGeneral general =
Source.GManyDecks ->
Json.field "deckCode" (Json.string |> Json.map ManyDecks.deckCode) |> Json.map Source.ManyDecks
Source.GJsonAgainstHumanity ->
Json.field "id" (JsonAgianstHumanity.idDecoder |> Json.map Source.JsonAgainstHumanity)
tokenValidity : Json.Decoder (List Lobby.Token)
tokenValidity =
@@ -24,6 +24,7 @@ module MassiveDecks.Models.Encoders exposing
import Dict
import Json.Encode as Json
import MassiveDecks.Card.Source.BuiltIn.Model as BuiltIn
import MassiveDecks.Card.Source.JsonAgainstHumanity.Model as JsonAgainstHumanity
import MassiveDecks.Card.Source.ManyDecks.Model as ManyDecks
import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Cast.Model as Cast
@@ -343,14 +344,18 @@ source s =
Source.ManyDecks deckCode ->
Json.object
[ ( "source", "ManyDecks" |> Json.string )
, ( "deckCode"
, deckCode |> ManyDecks.encode
)
, ( "deckCode", deckCode |> ManyDecks.encode )
]
Source.BuiltIn id ->
Json.object [ ( "source", "BuiltIn" |> Json.string ), ( "id", id |> BuiltIn.toString |> Json.string ) ]
Source.JsonAgainstHumanity id ->
Json.object
[ ( "source", "JAH" |> Json.string )
, ( "id", id |> JsonAgainstHumanity.toString |> Json.string )
]
language : Language -> Json.Value
language l =
+5 -1
View File
@@ -201,7 +201,11 @@ view shared model =
manyDecksAd { baseUrl } =
Html.blankA
[ HtmlA.href baseUrl, HtmlA.id "many-decks-ad", Strings.ManyDecksWhereToGet |> Lang.title shared ]
[ Html.div [] [ Icon.boxOpen |> Icon.viewIcon, Strings.ManyDecks |> Lang.html shared ] ]
[ Html.div []
[ Icon.boxOpen |> Icon.viewIcon
, Html.span [] [ Strings.ManyDecks |> Lang.string shared |> Html.text ]
]
]
in
[ Html.div [ HtmlA.class "page start" ]
[ overlay shared model.overlay
+2
View File
@@ -217,6 +217,8 @@ type MdString
| ManyDecksDeckCodeTitle -- A term referring to a deck code for Many Decks.
| ManyDecksDeckCodeShort -- A description of the problem where a deck code must be at least five characters.
| ManyDecksWhereToGet -- A description of how to get deck codes from Many Decks.
| JsonAgainstHumanity -- The name of the JSON Against Humanity source.
| JsonAgainstHumanityAbout -- A short description of the JSON Against Humanity source.
| BuiltIn -- A term referring to decks of cards that are provided by this instance of the game.
| APlayer -- A short description of a generic player in the game in the context of being the author of a card.
| DeckAlreadyAdded -- A description of the problem of the deck already being added to the game configuration.
@@ -757,7 +757,13 @@ translate mdString =
[ Text "A deck code must be at least five characters long." ]
ManyDecksWhereToGet ->
[ Text "You can create decks to play with at Many Decks." ]
[ Text "You can create and find decks to play with at ", Ref ManyDecks, Text "." ]
JsonAgainstHumanity ->
[ Text "JSON Against Humanity" ]
JsonAgainstHumanityAbout ->
[ Text "Decks provided by ", Ref JsonAgainstHumanity ]
BuiltIn ->
[ Text "Built-in" ]
@@ -786,6 +786,14 @@ translate mdString =
ManyDecksWhereToGet ->
[ Missing ]
-- TODO: Translate
JsonAgainstHumanity ->
[ Missing ]
-- TODO: Translate
JsonAgainstHumanityAbout ->
[ Missing ]
-- TODO: Translate
BuiltIn ->
[ Missing ]
@@ -773,7 +773,15 @@ translate mdString =
[ Text "Um código de deck deve ser de pelo menos cinco caracteres." ]
ManyDecksWhereToGet ->
[ Text "Você pode criar decks para jogar usando o Many Decks." ]
[ Text "Você pode criar decks para jogar usando o ", Ref ManyDecks, Text "." ]
-- TODO: Translate
JsonAgainstHumanity ->
[ Missing ]
-- TODO: Translate
JsonAgainstHumanityAbout ->
[ Missing ]
BuiltIn ->
[ Text "Embutido" ]
@@ -243,7 +243,7 @@ enhanceHtml context mdString unenhanced =
Played ->
term context PlayedDescription Icon.check unenhanced
ManyDecksWhereToGet ->
ManyDecks ->
case context.shared.sources.manyDecks of
Just { baseUrl } ->
[ Html.blankA [ HtmlA.href baseUrl ] unenhanced ]
@@ -251,6 +251,14 @@ enhanceHtml context mdString unenhanced =
Nothing ->
unenhanced
JsonAgainstHumanity ->
case context.shared.sources.jsonAgainstHumanity of
Just { aboutUrl } ->
[ Html.blankA [ HtmlA.href aboutUrl ] unenhanced ]
Nothing ->
unenhanced
_ ->
unenhanced
+2
View File
@@ -182,6 +182,8 @@
left: -0.8em;
top: -3em;
z-index: 11;
display: flex;
flex-direction: column-reverse;
align-items: center;
+10 -4
View File
@@ -32,15 +32,21 @@
decks: ["cah-base-en", "cah-base-ptbr"],
},
// Allows players to load decks from arbitrary URLs.
// Allows players to load decks from Many Decks.
manyDecks: {
baseUrl: "https://decks.rereadgames.com/",
// How long to wait for a response from the url before giving up and telling the user there is a problem.
// How long to wait for a response from Many Decks before giving up and telling the user there is a problem.
timeout: "PT10S",
// The number of connections the server can make to urls at one time.
simultaneousConnections: 1,
// The number of connections the server can make to Many Decks at one time.
simultaneousConnections: 2,
},
// Allows players to use decks from JSON Against Humanity
jsonAgainstHumanity: {
aboutUrl: "https://github.com/crhallberg/json-against-humanity",
url: "https://raw.githubusercontent.com/crhallberg/json-against-humanity/v2/compact.md.json",
},
},
@@ -334,6 +334,9 @@ export const Schema = {
{
$ref: "#/definitions/ManyDecks",
},
{
$ref: "#/definitions/JsonAgainstHumanity",
},
{
$ref: "#/definitions/BuiltIn",
},
@@ -388,6 +391,22 @@ export const Schema = {
description: "A unique id for a user.",
type: "string",
},
JsonAgainstHumanity: {
additionalProperties: false,
defaultProperties: [],
description: "From JSON Against Humanity (https://crhallberg.com/cah/)",
properties: {
id: {
type: "string",
},
source: {
enum: ["JAH"],
type: "string",
},
},
required: ["id", "source"],
type: "object",
},
Judge: {
additionalProperties: false,
defaultProperties: [],
+9
View File
@@ -56,9 +56,15 @@ interface BaseManyDecks<D extends Duration> {
}
export type ManyDecks = BaseManyDecks<ParsedDuration>;
export interface JsonAgainstHumanity {
aboutUrl: string;
url: string;
}
interface BaseSources<D extends Duration> {
builtIn?: BuiltIn;
manyDecks?: BaseManyDecks<D>;
jsonAgainstHumanity?: JsonAgainstHumanity;
}
export type Sources = BaseSources<ParsedDuration>;
@@ -159,6 +165,9 @@ const parseSources = (sources: BaseSources<UnparsedDuration>): Sources => ({
...(sources.manyDecks !== undefined
? { manyDecks: parseManyDecks(sources.manyDecks) }
: {}),
...(sources.jsonAgainstHumanity !== undefined
? { jsonAgainstHumanity: sources.jsonAgainstHumanity }
: {}),
});
export const pullFromEnvironment = (config: Parsed): Parsed => {
+11 -3
View File
@@ -54,6 +54,7 @@ export type Style = "Em" | "Strong";
/** An empty slot for responses to be played into.*/
export interface Slot {
index?: number;
/**
* Defines a transformation over the content the slot is filled with.
*/
@@ -95,7 +96,14 @@ export const isResponse = (card: Card): card is Response =>
/**
* The number of slots the given call.
*/
export const slotCount = (call: Call): number =>
wu(call.parts)
export const slotCount = (call: Call | Part[][]): number => {
let next = 0;
const indices = wu(
call.hasOwnProperty("parts") ? (call as Call).parts : (call as Part[][])
)
.flatten(true)
.reduce((count, part) => count + (isSlot(part) ? 1 : 0), 0);
.concatMap((part) =>
isSlot(part) ? [part.index !== undefined ? part.index : next++] : []
);
return new Set(indices).size;
};
+2 -1
View File
@@ -3,6 +3,7 @@ import * as Decks from "./decks";
import { Custom } from "./sources/custom";
import { BuiltIn } from "./sources/builtIn";
import { ManyDecks } from "./sources/many-decks";
import { JsonAgainstHumanity } from "./sources/json-against-humanity";
/**
* A source for a card or deck.
@@ -12,7 +13,7 @@ export type Source = External | Custom;
/**
* An external source for a card or deck.
*/
export type External = BuiltIn | ManyDecks;
export type External = BuiltIn | ManyDecks | JsonAgainstHumanity;
/**
* More information that can be looked up given a source.
+21 -4
View File
@@ -6,6 +6,7 @@ import * as Config from "../../config";
import * as BuiltIn from "./sources/builtIn";
import { SourceNotFoundError } from "../../errors/action-execution-error";
import * as ManyDecks from "./sources/many-decks";
import * as JsonAgainstHumanity from "./sources/json-against-humanity";
async function loadIfEnabled<Config, MetaResolver>(
config: Config | undefined,
@@ -21,21 +22,25 @@ async function loadIfEnabled<Config, MetaResolver>(
export interface ClientInfo {
builtIn?: BuiltIn.ClientInfo;
manyDecks?: ManyDecks.ClientInfo;
jsonAgainstHumanity?: JsonAgainstHumanity.ClientInfo;
}
export class Sources {
public readonly builtIn?: BuiltIn.MetaResolver;
public readonly manyDecks?: ManyDecks.MetaResolver;
public readonly jsonAgainstHumanity?: JsonAgainstHumanity.MetaResolver;
public constructor(
builtIn?: BuiltIn.MetaResolver,
manyDecks?: ManyDecks.MetaResolver
manyDecks?: ManyDecks.MetaResolver,
jsonAgainstHumanity?: JsonAgainstHumanity.MetaResolver
) {
if (builtIn === undefined && manyDecks === undefined) {
throw new Error("At least one source must be enabled.");
}
this.builtIn = builtIn;
this.manyDecks = manyDecks;
this.jsonAgainstHumanity = jsonAgainstHumanity;
}
public clientInfo(): ClientInfo {
@@ -48,6 +53,9 @@ export class Sources {
...(this.manyDecks !== undefined
? { manyDecks: this.manyDecks.clientInfo() }
: {}),
...(this.jsonAgainstHumanity !== undefined
? { jsonAgainstHumanity: this.jsonAgainstHumanity.clientInfo() }
: {}),
};
}
@@ -61,6 +69,9 @@ export class Sources {
case "ManyDecks":
return this.manyDecks;
case "JAH":
return this.jsonAgainstHumanity;
default:
Util.assertNever(source);
}
@@ -117,13 +128,19 @@ export class Sources {
};
public static async from(config: Config.Sources): Promise<Sources> {
const [builtInMeta, jsonUrlMeta] = await Promise.all<
const [
builtInMeta,
manyDecksMeta,
jsonAgainstHumanityMeta,
] = await Promise.all<
BuiltIn.MetaResolver | undefined,
ManyDecks.MetaResolver | undefined
ManyDecks.MetaResolver | undefined,
JsonAgainstHumanity.MetaResolver | undefined
>([
loadIfEnabled(config.builtIn, BuiltIn.load),
loadIfEnabled(config.manyDecks, ManyDecks.load),
loadIfEnabled(config.jsonAgainstHumanity, JsonAgainstHumanity.load),
]);
return new Sources(builtInMeta, jsonUrlMeta);
return new Sources(builtInMeta, manyDecksMeta, jsonAgainstHumanityMeta);
}
}
@@ -0,0 +1,262 @@
import * as Source from "../source";
import http, { AxiosRequestConfig } from "axios";
import * as Config from "../../../config";
import { SourceNotFoundError } from "../../../errors/action-execution-error";
import * as Decks from "../decks";
import * as Card from "../card";
/**
* From JSON Against Humanity (https://crhallberg.com/cah/)
*/
export interface JsonAgainstHumanity {
source: "JAH";
id: string;
}
export interface ClientInfo {
aboutUrl: string;
decks: {
id: string;
name: string;
}[];
}
interface RawDecks {
cards: {
white: { text: string }[];
black: { text: string; pick: number }[];
};
decks: { [id: string]: RawDeck };
}
interface RawDeck {
name: string;
description: string;
official: boolean;
icon: string | number;
white: number[];
black: number[];
}
const endsSentence = new Set([".", "!", "?"]);
function* introduceSlots(line: string): Iterable<Card.Part> {
const lineParts = line.split("_");
let nextSlot: Card.Part | undefined = undefined;
for (const part of lineParts) {
if (nextSlot !== undefined) {
yield nextSlot;
}
yield part;
const last = part.trimRight().substr(-1);
if (part === "" || endsSentence.has(last)) {
nextSlot = { transform: "Capitalize" };
} else {
nextSlot = {};
}
}
}
function rawDeckToSummaryAndTemplates(
raw: RawDecks,
id: string
): {
summary: Source.Summary;
templates: Decks.Templates;
} {
const pack = raw.decks[id];
const source: JsonAgainstHumanity = {
source: "JAH",
id,
};
function call(index: number): Card.Call {
const from = raw.cards.black[index];
const parts = from.text.split("\n").map((t) => [...introduceSlots(t)]);
const slots = Card.slotCount(parts);
const extraSlots = Math.max(0, from.pick - slots);
return {
id: Card.id(),
source,
parts: [...parts, ...Array(extraSlots).fill([{}])],
};
}
function response(index: number): Card.Response {
const from = raw.cards.white[index];
const stripped = from.text.replace("\n", "");
return {
id: Card.id(),
source,
text: stripped.endsWith(".")
? stripped.substr(0, stripped.length - 1)
: stripped,
};
}
return {
summary: {
details: { name: pack.name },
calls: pack.black.length,
responses: pack.white.length,
},
templates: {
calls: new Set(pack.black.map(call)),
responses: new Set(pack.white.map(response)),
},
};
}
export class Resolver extends Source.Resolver<JsonAgainstHumanity> {
public readonly source: JsonAgainstHumanity;
private readonly config: Config.JsonAgainstHumanity;
private readonly storedSummary: Source.Summary;
private readonly storedTemplates: Decks.Templates;
public constructor(
source: JsonAgainstHumanity,
config: Config.JsonAgainstHumanity,
summary: Source.Summary,
templates: Decks.Templates
) {
super();
this.source = source;
this.config = config;
this.storedSummary = summary;
this.storedTemplates = templates;
}
public id(): string {
return "JAH";
}
public deckId(): string {
return this.source.id;
}
public loadingDetails(): Source.Details {
return {
name: this.storedSummary.details.name,
};
}
public equals(source: Source.External): boolean {
return source.source === "JAH" && this.source.id === source.id;
}
public async getTag(): Promise<string | undefined> {
return (await this.summary()).tag;
}
public async atLeastSummary(): Promise<Source.AtLeastSummary> {
return await this.summaryAndTemplates();
}
public async atLeastTemplates(): Promise<Source.AtLeastTemplates> {
return await this.summaryAndTemplates();
}
public summaryAndTemplates = async (): Promise<{
summary: Source.Summary;
templates: Decks.Templates;
}> => ({
summary: this.storedSummary,
templates: this.storedTemplates,
});
}
export class MetaResolver implements Source.MetaResolver<JsonAgainstHumanity> {
private readonly config: Config.JsonAgainstHumanity;
private readonly decks: Map<
string,
{
summary: Source.Summary;
templates: Decks.Templates;
}
>;
private readonly order: string[];
public readonly cache = false;
public constructor(config: Config.JsonAgainstHumanity, decks: RawDecks) {
this.config = config;
this.decks = new Map();
const protoOrder: [string, RawDeck][] = [];
for (const id in decks.decks) {
this.decks.set(id, rawDeckToSummaryAndTemplates(decks, id));
protoOrder.push([id, decks.decks[id]]);
}
protoOrder.sort(MetaResolver.compare);
this.order = protoOrder.map(([id, _]) => id);
}
private static compare(
[_idA, a]: [string, RawDeck],
[_idB, b]: [string, RawDeck]
): number {
const official = MetaResolver.boolCompare(a, b, (v) => v.official);
if (official !== 0) {
return official;
} else {
const isThirdParty = MetaResolver.boolCompare(a, b, (v) =>
v.name.startsWith("[$]")
);
if (isThirdParty !== 0) {
return isThirdParty;
} else {
const isCommunity = MetaResolver.boolCompare(a, b, (v) =>
v.name.startsWith("[C]")
);
if (isCommunity !== 0) {
return isCommunity;
} else {
return 0;
}
}
}
}
private static boolCompare<T>(a: T, b: T, f: (t: T) => boolean): number {
const x = f(a);
const y = f(b);
return x && !y ? -1 : y && !x ? 1 : 0;
}
public clientInfo(): ClientInfo {
return {
aboutUrl: this.config.aboutUrl,
decks: this.order.map((id) => ({
id,
name: (this.decks.get(id) as { summary: Source.Summary }).summary
.details.name,
})),
};
}
limitedResolver(source: JsonAgainstHumanity): Resolver {
return this.resolver(source);
}
resolver(source: JsonAgainstHumanity): Resolver {
const deck = this.decks.get(source.id);
if (deck !== undefined) {
const { summary, templates } = deck;
return new Resolver(source, this.config, summary, templates);
} else {
throw new SourceNotFoundError(source);
}
}
}
export const load = async (
config: Config.JsonAgainstHumanity
): Promise<MetaResolver> => {
const httpConfig: AxiosRequestConfig = {
method: "GET",
baseURL: config.url,
responseType: "json",
};
const data = await http.get("", httpConfig);
return new MetaResolver(config, data.data);
};