diff --git a/lib/viral_spiral/affinity.ex b/lib/viral_spiral/affinity.ex index ae3dc9a..0514d2f 100644 --- a/lib/viral_spiral/affinity.ex +++ b/lib/viral_spiral/affinity.ex @@ -1,8 +1,27 @@ defmodule ViralSpiral.Affinity do + alias ViralSpiral.Affinity + import ViralSpiral.Game.EngineConfig.Guards + defstruct target: nil - @type target :: :cat | :sock | :highfive | :houseboat | :skub + @labels %{ + cat: "Cat", + sock: "Sock", + high_five: "High Five", + houseboat: "Houseboat", + skub: "Skub" + } + + @type target :: :cat | :sock | :high_five | :houseboat | :skub @type t :: %__MODULE__{ target: target() } + + def label(target) when is_affinity(target) do + @labels[target] + end + + def label(%Affinity{} = affinity) do + @labels[affinity.target] + end end diff --git a/lib/viral_spiral/bias.ex b/lib/viral_spiral/bias.ex index b6385d2..0c06db8 100644 --- a/lib/viral_spiral/bias.ex +++ b/lib/viral_spiral/bias.ex @@ -1,8 +1,24 @@ defmodule ViralSpiral.Bias do + alias ViralSpiral.Bias + import ViralSpiral.Game.EngineConfig.Guards defstruct target: nil @type target :: :red | :yellow | :blue @type t :: %__MODULE__{ target: target() } + + @labels %{ + red: "Red", + yellow: "Yellow", + blue: "Blue" + } + + def label(target) when is_community(target) do + @labels[target] + end + + def label(%Bias{} = affinity) do + @labels[affinity.target] + end end diff --git a/lib/viral_spiral/canon/deck.ex b/lib/viral_spiral/canon/deck.ex index 8020bc4..4bdc7b0 100644 --- a/lib/viral_spiral/canon/deck.ex +++ b/lib/viral_spiral/canon/deck.ex @@ -136,7 +136,7 @@ defmodule ViralSpiral.Canon.Deck do Enum.reduce( cards, %{}, - &Map.put(&2, &1.id, &1) + &Map.put(&2, {&1.id, &1.veracity}, &1) ) end @@ -315,7 +315,7 @@ defmodule ViralSpiral.Canon.Deck do target: :sock, veracity: false, polarity: :positive, - headline: Enum.at(row, @columns.pro_sock), + headline: Enum.at(row, @columns.pro_sock_fake), image: Enum.at(row, @columns.pro_sock_image) }, %Affinity{ @@ -333,7 +333,7 @@ defmodule ViralSpiral.Canon.Deck do target: :sock, veracity: false, polarity: :negative, - headline: Enum.at(row, @columns.anti_sock), + headline: Enum.at(row, @columns.anti_sock_fake), image: Enum.at(row, @columns.anti_sock_image) }, %Affinity{ @@ -351,7 +351,7 @@ defmodule ViralSpiral.Canon.Deck do target: :skub, veracity: false, polarity: :positive, - headline: Enum.at(row, @columns.pro_skub), + headline: Enum.at(row, @columns.pro_skub_fake), image: Enum.at(row, @columns.pro_skub_image) }, %Affinity{ @@ -369,7 +369,7 @@ defmodule ViralSpiral.Canon.Deck do target: :skub, veracity: false, polarity: :negative, - headline: Enum.at(row, @columns.anti_skub), + headline: Enum.at(row, @columns.anti_skub_fake), image: Enum.at(row, @columns.anti_skub_image) }, %Affinity{ @@ -387,7 +387,7 @@ defmodule ViralSpiral.Canon.Deck do target: :highfive, veracity: false, polarity: :positive, - headline: Enum.at(row, @columns.pro_high_five), + headline: Enum.at(row, @columns.pro_high_five_fake), image: Enum.at(row, @columns.pro_high_five_image) }, %Affinity{ @@ -405,7 +405,7 @@ defmodule ViralSpiral.Canon.Deck do target: :highfive, veracity: false, polarity: :negative, - headline: Enum.at(row, @columns.anti_highfive), + headline: Enum.at(row, @columns.anti_highfive_fake), image: Enum.at(row, @columns.anti_highfive_image) }, %Affinity{ @@ -456,7 +456,12 @@ defmodule ViralSpiral.Canon.Deck do ] end - defp card_id(headline) do + @doc """ + Generate a hash of the card headline. + + Throughout the csv files, viral spiral writers use the card headline as a link between various sheets and rows. + """ + def card_id(headline) do "card_" <> Integer.to_string(:erlang.phash2(headline)) end @@ -523,7 +528,9 @@ defmodule ViralSpiral.Canon.Deck do end defp choose_one(list) do - list |> Enum.shuffle() |> Enum.take(1) |> hd |> Map.get(:id) + ix = :rand.uniform(list |> Enum.to_list() |> length) + + list |> Enum.at(ix) |> Map.get(:id) end def key(card) do @@ -613,14 +620,26 @@ defmodule ViralSpiral.Canon.Deck do |> Enum.map(fn card -> try do if card.type == :conflated do - IO.inspect(card) + # IO.inspect(card) end - article_id = articles[{card.id, card.veracity}] - %{card | article_id: article_id} + article = articles[{card.id, card.veracity}] + %{card | article_id: article.id} rescue KeyError -> card end end) end + + def get_fake_card(store, card_id) when is_bitstring(card_id) do + store[{card_id, false}] + end + + def get_fake_card(store, card) do + store[{card.id, false}] + end + + def substitute_text(state, card) do + card + end end diff --git a/lib/viral_spiral/canon/dynamic_card.ex b/lib/viral_spiral/canon/dynamic_card.ex new file mode 100644 index 0000000..cd4be29 --- /dev/null +++ b/lib/viral_spiral/canon/dynamic_card.ex @@ -0,0 +1,68 @@ +defmodule ViralSpiral.Canon.DynamicCard do + @moduledoc """ + Changes card text dynamically. + + Viral Spiral card texts have placeholders that get replaced at runtime by the game engine. We call these cards to be having dynamic text. Some examples of dynamic texts in cards are : + - City revokes docking privileges for (other community) HoBos - "If they like the water so much they can stay there! + - (oppressed community) ghetto vandalized, burned down during hilarious (popular affinity) day parade hooliganism + + Supported placeholders are : (other community), (dominant community), (oppressed community), (unpopular affinity), (popular affinity). + + ## Example Usage + headline = "People who like (unpopular affinity) are usually (dominant community)" + matches = DynamicCard.find_placeholders(headline) + replacements = %{ + unpopular_affinity: :skub, + dominant_community: :red + } + + new_headline = DynamicCard.replace_text(headline, matches, replacements) + + In practice you'd require visibility into the game state to create the replacements map show above. This falls under the responsibility of `ViralSpiral.Room.State.Analytics` + """ + alias ViralSpiral.Bias + alias ViralSpiral.Affinity + + @mappings [ + {"(other community)", :other_community}, + {"(dominant community)", :dominant_community}, + {"(oppressed community)", :oppressed_community}, + {"(unpopular affinity)", :unpopular_affinity}, + {"(popular affinity)", :popular_affinity} + ] + @string_to_atom_map Enum.reduce(@mappings, %{}, fn x, acc -> + Map.put(acc, elem(x, 0), elem(x, 1)) + end) + @atom_to_string_map Enum.reduce(@mappings, %{}, fn x, acc -> + Map.put(acc, elem(x, 1), elem(x, 0)) + end) + + def find_placeholders(headline) do + results = + Regex.scan( + ~r/(\(oppressed community\)|\(popular affinity\)|\(unpopular affinity\)|\(other community\)|\(dominant community\))/, + headline + ) + + results + |> Enum.map(&Enum.at(&1, 0)) + |> Enum.map(&@string_to_atom_map[&1]) + end + + def replace_text(headline, matches, replacements) do + Enum.reduce( + matches, + headline, + fn el, acc -> + String.replace(acc, @atom_to_string_map[el], label(replacements[el])) + end + ) + end + + defp label(atom) do + case atom do + x when x in [:cat, :skub, :high_five, :houseboat, :sock] -> Affinity.label(atom) + y when y in [:red, :yellow, :blue] -> Bias.label(atom) + end + end +end diff --git a/lib/viral_spiral/canon/encyclopedia.ex b/lib/viral_spiral/canon/encyclopedia.ex index 840bf3c..e7df260 100644 --- a/lib/viral_spiral/canon/encyclopedia.ex +++ b/lib/viral_spiral/canon/encyclopedia.ex @@ -59,6 +59,10 @@ defmodule ViralSpiral.Canon.Encyclopedia do """ def create_store(articles) do articles - |> Enum.reduce(%{}, fn el, acc -> Map.put(acc, {el.card_id, el.veracity}, el.id) end) + |> Enum.reduce(%{}, fn el, acc -> Map.put(acc, {el.card_id, el.veracity}, el) end) + end + + def get_article_by_card(article_store, card) do + article_store[{card.id, card.veracity}] end end diff --git a/lib/viral_spiral/card_share.ex b/lib/viral_spiral/card_share.ex index 3986d29..566e74e 100644 --- a/lib/viral_spiral/card_share.ex +++ b/lib/viral_spiral/card_share.ex @@ -15,7 +15,7 @@ end defimpl ViralSpiral.CardShare, for: ViralSpiral.Canon.Card.Topical do def pass(card, state, from, to) do - IO.inspect("returning changes for Bias card") + # IO.inspect("returning changes for Bias card") end def keep(card, state, from) do @@ -73,7 +73,7 @@ end defimpl ViralSpiral.CardShare, for: ViralSpiral.Canon.Card.Conflated do def pass(card, state, from, to) do - IO.inspect("returning changes for Bias card") + # IO.inspect("returning changes for Bias card") end def keep(card, state, from) do diff --git a/lib/viral_spiral/room/state/analytics.ex b/lib/viral_spiral/room/state/analytics.ex new file mode 100644 index 0000000..a41b258 --- /dev/null +++ b/lib/viral_spiral/room/state/analytics.ex @@ -0,0 +1,7 @@ +defmodule ViralSpiral.Room.State.Analytics do + @moduledoc """ + Find insights into the room durin gameplay. + + As part of the gameplay we often need to know information like who is winning the game or which community is performing well etc. This module provides helpers for that + """ +end diff --git a/lib/viral_spiral/room/state/player.ex b/lib/viral_spiral/room/state/player.ex index 2afea2b..7c6db13 100644 --- a/lib/viral_spiral/room/state/player.ex +++ b/lib/viral_spiral/room/state/player.ex @@ -9,6 +9,8 @@ defmodule ViralSpiral.Room.State.Player do clout: 0 } """ + alias ViralSpiral.Bias + alias ViralSpiral.Affinity alias ViralSpiral.Room.State.Player alias ViralSpiral.Game.EngineConfig alias ViralSpiral.Game.Player, as: PlayerData @@ -25,8 +27,8 @@ defmodule ViralSpiral.Room.State.Player do } @spec new(t(), %ViralSpiral.Game.EngineConfig{ - :affinities => list(), - :communities => list() + :affinities => list(Affinity.t()), + :communities => list(Bias.t()) }) :: t() def new(%PlayerData{} = player, %EngineConfig{} = room_config) do bias_list = Enum.filter(room_config.communities, &(&1 != player.identity)) diff --git a/test/viral_spiral/canon/dynamic_card_text.exs b/test/viral_spiral/canon/dynamic_card_text.exs new file mode 100644 index 0000000..cfaf152 --- /dev/null +++ b/test/viral_spiral/canon/dynamic_card_text.exs @@ -0,0 +1,52 @@ +defmodule ViralSpiral.Canon.DynamicCardTest do + use ExUnit.Case + alias ViralSpiral.Canon.DynamicCard + + test "find placeholder text for (other community)" do + headline = "(other community) club shover crime spree continues - another rave in disarray" + matches = DynamicCard.find_placeholders(headline) + assert Enum.at(matches, 0) == :other_community + end + + test "find placeholder text for (dominant community)" do + headline = "(dominant community) club shover crime spree continues - another rave in disarray" + matches = DynamicCard.find_placeholders(headline) + assert Enum.at(matches, 0) == :dominant_community + end + + test "find placeholder text for (oppressed community)" do + headline = + "City announces discriminatory rating system to filter 'productive' members of (oppressed community) community from others" + + matches = DynamicCard.find_placeholders(headline) + assert Enum.at(matches, 0) == :oppressed_community + end + + test "find placeholder text for (popuar affinity)" do + headline = "People who like (popular affinity) are good" + + matches = DynamicCard.find_placeholders(headline) + assert Enum.at(matches, 0) == :popular_affinity + end + + test "find placeholder text for (unpopuar affinity)" do + headline = "People who like (unpopular affinity) are good" + + matches = DynamicCard.find_placeholders(headline) + assert Enum.at(matches, 0) == :unpopular_affinity + end + + test "replace multiple placeholder text" do + headline = "People who like (unpopular affinity) are usually (dominant community)" + + matches = DynamicCard.find_placeholders(headline) + + replacements = %{ + unpopular_affinity: :skub, + dominant_community: :red + } + + new_headline = DynamicCard.replace_text(headline, matches, replacements) + assert new_headline == "People who like Skub are usually Red" + end +end diff --git a/test/viral_spiral/canon_test.exs b/test/viral_spiral/canon_test.exs index fa43604..9a3388d 100644 --- a/test/viral_spiral/canon_test.exs +++ b/test/viral_spiral/canon_test.exs @@ -1,31 +1,89 @@ defmodule ViralSpiral.CanonTest do + use ExUnit.Case alias ViralSpiral.Canon.Encyclopedia alias ViralSpiral.Canon.Deck - describe "deck integrity" do + describe "card data integrity" do end describe "deck functions" do setup do + :rand.seed(:exsss, {12356, 123_534, 345_345}) cards = Deck.load_cards() + articles = Encyclopedia.load_articles() + article_store = Encyclopedia.create_store(articles) + cards = Deck.link(cards, article_store) + store = Deck.create_store(cards) sets = Deck.create_sets(cards) - articles = Encyclopedia.load_articles() - article_store = Encyclopedia.create_store(articles) + %{cards: cards, store: store, sets: sets, articles: articles, article_store: article_store} + end - cards = Deck.link(cards, article_store) + test "drawing a card should reduce available cards by 1" do + end + + test "turn card to fake", state do + set = state[:sets] + store = state[:store] + article_store = state[:article_store] - %{cards: cards, articles: articles} + card_id = Deck.draw_card(set, type: :affinity, veracity: true, tgb: 5, target: :sock) + card = store[{card_id, true}] + assert card.headline == "Socks attract rats to your house, beware" + + fake_card = Deck.get_fake_card(store, card.id) + assert fake_card.headline == "Socks attract rats, (other community)s to your house, beware" end - test "turn card to fake" do + test "lookup a card's encyclopedia entry", state do + set = state[:sets] + store = state[:store] + article_store = state[:article_store] + + card_id = Deck.draw_card(set, type: :topical, veracity: true, tgb: 5) + card = store[{card_id, true}] + assert card.id == "card_27424926" + assert card.veracity == true + + article = Encyclopedia.get_article_by_card(article_store, card) + + assert article.headline == + "Global pocket shortage finally hits City, driving up prices of dresses with pockets" + + assert article.veracity == true end + end - test "lookup a card's encyclopedia entry" do + describe "dynamic text on card" do + setup do + :rand.seed(:exsss, {12356, 123_534, 345_345}) + + game = Fixtures.initialized_game() + + cards = Deck.load_cards() + store = Deck.create_store(cards) + sets = Deck.create_sets(cards) + + %{cards: cards, store: store, sets: sets, game: game} end - test "card text replacement for dynamic cards" do + test "card text replacement for card with dominant community", state do + # todo game needs helper functions for + # oppressed community, dominant community etc + game_state = state[:game] + player_scores = game_state.player_scores + card_store = state[:store] + + headline = "(dominant capes) hold march through City, police joins in" + card_id = Deck.card_id(headline) + card = card_store[{card_id, true}] + + card = Deck.substitute_text(game_state, card) |> IO.inspect() + assert String.contains?(card.headline, "(dominant capes)") == false + + # players = Fixtures.player_list() |> IO.inspect() + # player_score_list = Fixtures.player_score_list(players) end end end