diff --git a/config/config.exs b/config/config.exs index 3157f5c..bd204f6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -68,6 +68,8 @@ config :viral_spiral, RoomConfig, chaos_counter: 10, volatility: :medium +config :viral_spiral, CardConfig, card_types: [:affinity, :bias, :topical, :conflated] + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/lib/viral_spiral/deck/card.ex b/lib/viral_spiral/deck/card.ex new file mode 100644 index 0000000..8efef0b --- /dev/null +++ b/lib/viral_spiral/deck/card.ex @@ -0,0 +1,21 @@ +defmodule ViralSpiral.Deck.Card do + import ViralSpiral.Deck.CardGuards + alias ViralSpiral.Deck.Card + + defstruct id: nil, + type: nil + + @type card_types :: :affinity | :bias | :topical | :conflated + + @type t :: %__MODULE__{ + id: String.t(), + type: card_types() + } + + def new(type) when is_card_type(type) do + %Card{ + id: UXID.generate!(prefix: "card", size: :small), + type: type + } + end +end diff --git a/lib/viral_spiral/deck/card_content.ex b/lib/viral_spiral/deck/card_content.ex new file mode 100644 index 0000000..abb5d4c --- /dev/null +++ b/lib/viral_spiral/deck/card_content.ex @@ -0,0 +1,6 @@ +defmodule ViralSpiral.Deck.CardContent do + defstruct title: "", + description: "", + fake_title: "", + fake_description: "" +end diff --git a/lib/viral_spiral/deck/card_guards.ex b/lib/viral_spiral/deck/card_guards.ex new file mode 100644 index 0000000..fad74e2 --- /dev/null +++ b/lib/viral_spiral/deck/card_guards.ex @@ -0,0 +1,5 @@ +defmodule ViralSpiral.Deck.CardGuards do + @card_types Application.compile_env(:viral_spiral, CardConfig)[:card_types] + + defguard is_card_type(value) when value in @card_types +end diff --git a/lib/viral_spiral/game.ex b/lib/viral_spiral/game.ex index 1808377..d7bc17d 100644 --- a/lib/viral_spiral/game.ex +++ b/lib/viral_spiral/game.ex @@ -1,4 +1,11 @@ defmodule ViralSpiral.Game do + @moduledoc """ + Context for Game + """ + alias ViralSpiral.Game.State + alias ViralSpiral.Game.Score.Player + alias ViralSpiral.Game.Player + alias ViralSpiral.Deck.Card alias ViralSpiral.Game.Room @spec create_room(String.t()) :: Room.t() @@ -9,7 +16,17 @@ defmodule ViralSpiral.Game do def join_room(_name, _password) do end - def pass_card(_state, _player, _to) do + @doc """ + Pass a card from one player to another. + """ + # @spec pass_card(Player.t(), Card.t()) :: list(Change.t()) + def pass_card(state, %Card{} = card, %Player{} = from, %Player{} = _to) do + changes = + case card.type do + :affinity -> [{state.player_scores[from.id], [type: :inc, target: :affinity, count: 1]}] + end + + State.apply_changes(state, changes) end def keep_card(_player) do @@ -24,6 +41,9 @@ defmodule ViralSpiral.Game do def cancel_player(_player, _target) do end + def cancel_player_vote(_player, _target) do + end + def viral_spiral(_player, _targets) do end end diff --git a/lib/viral_spiral/game/_change.ex b/lib/viral_spiral/game/_change.ex new file mode 100644 index 0000000..9c0d021 --- /dev/null +++ b/lib/viral_spiral/game/_change.ex @@ -0,0 +1,14 @@ +# defmodule ViralSpiral.Game.Change do +# defstruct type: nil, +# target: nil, +# target_id: nil + +# @type change_type :: :inc | :dec | :value +# @type target :: :chaos_counter | :clout | :affinity | :bias + +# @type t :: %__MODULE__{ +# type: change_type(), +# target: target(), +# target_id: String.t() +# } +# end diff --git a/lib/viral_spiral/game/player.ex b/lib/viral_spiral/game/player.ex index 8195e9a..43d30c9 100644 --- a/lib/viral_spiral/game/player.ex +++ b/lib/viral_spiral/game/player.ex @@ -1,4 +1,5 @@ defmodule ViralSpiral.Game.Player do + alias ViralSpiral.Deck.Card alias ViralSpiral.Game.Player alias ViralSpiral.Game.RoomConfig @@ -7,6 +8,13 @@ defmodule ViralSpiral.Game.Player do identity: nil, hand: [] + @type t :: %__MODULE__{ + id: String.t(), + name: String.t(), + identity: atom(), + hand: list(Card.t()) + } + def new() do %Player{ id: UXID.generate!(prefix: "player", size: :small) diff --git a/lib/viral_spiral/game/score.ex b/lib/viral_spiral/game/score.ex deleted file mode 100644 index e3a6f02..0000000 --- a/lib/viral_spiral/game/score.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule ViralSpiral.Game.Score.Room do - alias ViralSpiral.Game.Score.Room - defstruct chaos_countdown: 10 - - def new() do - %Room{} - end - - def countdown(%Room{} = room) do - %{room | chaos_countdown: room.chaos_countdown - 1} - end -end - -defmodule ViralSpiral.Game.Score.Player do - alias ViralSpiral.Game.Score.Player - alias ViralSpiral.Game.RoomConfig - alias ViralSpiral.Game.Player, as: PlayerData - import ViralSpiral.Game.RoomConfig.Guards - - defstruct biases: %{}, affinities: %{}, clout: 0 - - def new(%PlayerData{} = player, %RoomConfig{} = room_config) do - bias_list = Enum.filter(room_config.communities, &(&1 != player.identity)) - bias_map = Enum.reduce(bias_list, %{}, fn x, acc -> Map.put(acc, x, 0) end) - - affinity_list = room_config.affinities - affinity_map = Enum.reduce(affinity_list, %{}, fn x, acc -> Map.put(acc, x, 0) end) - - %Player{ - biases: bias_map, - affinities: affinity_map - } - end - - def change(%Player{} = player, :bias, target_bias, count) - when is_community(target_bias) and is_integer(count) do - new_biases = Map.put(player.biases, target_bias, player.biases[target_bias] + count) - %{player | biases: new_biases} - end - - def change(%Player{} = player, :affinity, target, count) - when is_affinity(target) and is_integer(count) do - new_affinities = Map.put(player.affinities, target, player.affinities[target] + count) - %{player | affinities: new_affinities} - end - - def change(%Player{} = player, :clout, count) when is_integer(count) do - new_clout = player.clout + count - %{player | clout: new_clout} - end -end diff --git a/lib/viral_spiral/game/state.ex b/lib/viral_spiral/game/state.ex index f6e016a..9075391 100644 --- a/lib/viral_spiral/game/state.ex +++ b/lib/viral_spiral/game/state.ex @@ -18,6 +18,7 @@ defmodule ViralSpiral.Game.State do When a round begins, we also start a Turn. Within each Round there's a turn that includes everyone except the person who started the turn. """ alias ViralSpiral.Game.State + alias ViralSpiral.Game.Change def set_round(%State{} = game, round) do %State{game | round: round} @@ -26,4 +27,28 @@ defmodule ViralSpiral.Game.State do def set_turn(%State{} = game, turn) do %State{game | turn: turn} end + + # @spec apply_changes(list(Change.t())) :: + # list({:ok, message :: String.t()} | {:error, reason :: String.t()}) + def apply_changes(state, changes) do + # Enum.reduce(changes, [], &(&2 ++ [apply_change(elem(&1, 0), elem(&1, 1))])) + _results = Enum.map(changes, &apply(elem(&1, 0), elem(&1, 1))) + # new_state = Enum.reduce(results, state, &) + + # results = Enum.reduce... + # state = Enum.reduce(results, state, &Map.put(&1.id, &1.value)) + end + + defdelegate apply_change(change, opts), to: Change + + # @doc """ + # Change various components of state. + + # round, turn, room, card, player_score + # """ + # def apply(state, change) do + # case change do + # %{id: id, value: value} + # end + # end end diff --git a/lib/viral_spiral/game/turn.ex b/lib/viral_spiral/game/turn.ex index 6586a49..b4e14f6 100644 --- a/lib/viral_spiral/game/turn.ex +++ b/lib/viral_spiral/game/turn.ex @@ -14,7 +14,7 @@ defmodule ViralSpiral.Game.Turn do pass_to: [] @type t :: %__MODULE__{ - current: String.t(), + current: String.t() | nil, pass_to: list(String.t()) } diff --git a/lib/viral_spiral/score/change.ex b/lib/viral_spiral/score/change.ex new file mode 100644 index 0000000..4184fe9 --- /dev/null +++ b/lib/viral_spiral/score/change.ex @@ -0,0 +1,10 @@ +defprotocol ViralSpiral.Score.Change do + @moduledoc """ + Protocol to change scores used in Viral Spiral. + + ## Fields + - score: struct which implements the `Change` protocol + - change_description: a Keyword List with parameters defining the change + """ + def apply_change(score, change_description) +end diff --git a/lib/viral_spiral/score/player.ex b/lib/viral_spiral/score/player.ex new file mode 100644 index 0000000..2b28c37 --- /dev/null +++ b/lib/viral_spiral/score/player.ex @@ -0,0 +1,70 @@ +defmodule ViralSpiral.Score.Player do + @moduledoc """ + Create and update Player Score. + + ## Example + iex> player_score = %ViralSpiral.Game.Score.Player{ + biases: %{red: 0, blue: 0}, + affinities: %{cat: 0, sock: 0}, + clout: 0 + } + """ + alias ViralSpiral.Score.Player + alias ViralSpiral.Game.RoomConfig + alias ViralSpiral.Game.Player, as: PlayerData + import ViralSpiral.Game.RoomConfig.Guards + alias ViralSpiral.Score.Change + + defstruct biases: %{}, affinities: %{}, clout: 0 + + @type change_opts :: [type: :clout | :affinity | :bias, offset: integer(), target: atom()] + @type t :: %__MODULE__{ + biases: map(), + affinities: map(), + clout: integer() + } + + def new(%PlayerData{} = player, %RoomConfig{} = room_config) do + bias_list = Enum.filter(room_config.communities, &(&1 != player.identity)) + bias_map = Enum.reduce(bias_list, %{}, fn x, acc -> Map.put(acc, x, 0) end) + + affinity_list = room_config.affinities + affinity_map = Enum.reduce(affinity_list, %{}, fn x, acc -> Map.put(acc, x, 0) end) + + %Player{ + biases: bias_map, + affinities: affinity_map + } + end + + defimpl Change do + @doc """ + Implement change protocol for a Player's Score. + """ + @spec apply_change(Player.t(), Player.change_opts()) :: Player.t() + def apply_change(player, opts) do + case opts[:type] do + :clout -> change(player, :clout, opts[:offset]) + :affinity -> change(player, :affinity, opts[:target], opts[:offset]) + :bias -> change(player, :bias, opts[:target], opts[:offset]) + end + end + + def change(%Player{} = player, :bias, target_bias, count) + when is_community(target_bias) and is_integer(count) do + new_biases = Map.put(player.biases, target_bias, player.biases[target_bias] + count) + %{player | biases: new_biases} + end + + def change(%Player{} = player, :affinity, target, count) + when is_affinity(target) and is_integer(count) do + new_affinities = Map.put(player.affinities, target, player.affinities[target] + count) + %{player | affinities: new_affinities} + end + + def change(%Player{} = player, :clout, count) when is_integer(count) do + new_clout = player.clout + count + %{player | clout: new_clout} + end + end +end diff --git a/lib/viral_spiral/score/room.ex b/lib/viral_spiral/score/room.ex new file mode 100644 index 0000000..d1234e1 --- /dev/null +++ b/lib/viral_spiral/score/room.ex @@ -0,0 +1,38 @@ +defmodule ViralSpiral.Score.Room do + @moduledoc """ + ## Example + + """ + alias ViralSpiral.Score.Change + alias ViralSpiral.Score.Room + defstruct chaos_countdown: 10 + + @type t :: %__MODULE__{ + chaos_countdown: integer() + } + + @spec new() :: ViralSpiral.Score.Room.t() + def new() do + %Room{} + end + + @spec countdown(ViralSpiral.Score.Room.t()) :: ViralSpiral.Score.Room.t() + def countdown(%Room{} = room) do + %{room | chaos_countdown: room.chaos_countdown - 1} + end + + defimpl Change do + @spec apply_change(ViralSpiral.Score.Room.t(), keyword()) :: ViralSpiral.Score.Room.t() + def apply_change(%Room{} = score, opts) do + opts = Keyword.validate!(opts, offset: 0) + + case opts[:offset] do + x when is_integer(x) -> + Map.put(score, :chaos_countdown, score.chaos_countdown + opts[:offset]) + + y when is_bitstring(y) -> + Map.put(score, :chaos_countdown, score.chaos_countdown) + end + end + end +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 90f2988..852ac1c 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -1,4 +1,5 @@ defmodule Fixtures do + alias ViralSpiral.Deck.Card alias ViralSpiral.Game.Score.Player alias ViralSpiral.Game.Turn alias ViralSpiral.Game.Round @@ -10,7 +11,7 @@ defmodule Fixtures do alias ViralSpiral.Game.State def initialized_game() do - room_config = %RoomConfig{} |> IO.inspect() + room_config = %RoomConfig{} player_list = [ Player.new(room_config) |> Player.set_name("adhiraj"), @@ -24,6 +25,17 @@ defmodule Fixtures do round = Round.new(player_list) turn = Turn.new(round) + player_score_list = + Enum.map( + player_list, + &(Map.new() |> Map.put(:id, &1.id) |> Map.put(:score, PlayerScore.new(&1, room_config))) + ) + + player_score_map = + Enum.reduce(player_score_list, %{}, fn player, acc -> + Map.put(acc, player.id, player.score) + end) + %State{ room_config: room_config, room: Room.new(), @@ -32,7 +44,11 @@ defmodule Fixtures do round: round, turn: turn, # room_score: RoomScore.new(), - player_scores: Enum.map(player_list, &PlayerScore.new(&1, room_config)) + player_scores: player_score_map } end + + def card_affinity() do + Card.new(:affinity) + end end diff --git a/test/viral_spiral/gameplay_test.exs b/test/viral_spiral/gameplay_test.exs index 8b989cb..7aad115 100644 --- a/test/viral_spiral/gameplay_test.exs +++ b/test/viral_spiral/gameplay_test.exs @@ -1,4 +1,5 @@ defmodule ViralSpiral.GameTest do + alias ViralSpiral.Game use ExUnit.Case describe "card actions" do @@ -7,12 +8,22 @@ defmodule ViralSpiral.GameTest do %{state: game_state} end - test "passing an affinity card", %{state: game_state} do + test "passing an affinity card changes the player's clout and affinity", %{state: game_state} do players = game_state.player_map round = game_state.round turn = game_state.turn room_score = game_state.room_score player_scores = game_state.player_scores + + card = Fixtures.card_affinity() + + current_player = players[turn.current] + target_player = players[2] + + Game.pass_card(game_state, card, current_player, target_player) + + IO.inspect(game_state) + # IO.inspect(card) end end end diff --git a/test/viral_spiral/score/player_text.exs b/test/viral_spiral/score/player_text.exs new file mode 100644 index 0000000..a651e58 --- /dev/null +++ b/test/viral_spiral/score/player_text.exs @@ -0,0 +1,30 @@ +defmodule ViralSpiral.Score.PlayerTest do + alias ViralSpiral.Score.Change + alias ViralSpiral.Score.Player + use ExUnit.Case + + setup_all do + player = %Player{ + biases: %{red: 0, blue: 0}, + affinities: %{cat: 0, sock: 0}, + clout: 0 + } + + %{player: player} + end + + test "change player clout", %{player: player} do + new_player = Change.apply_change(player, type: :clout, offset: 5) + assert new_player.clout == 5 + end + + test "change player affinity", %{player: player} do + new_player = Change.apply_change(player, type: :affinity, target: :cat, offset: -2) + assert new_player.affinities.cat == -2 + end + + test "change player bias", %{player: player} do + new_player = Change.apply_change(player, type: :bias, target: :red, offset: 9) + assert new_player.biases.red == 9 + end +end diff --git a/test/viral_spiral/score/room_test.exs b/test/viral_spiral/score/room_test.exs new file mode 100644 index 0000000..4030005 --- /dev/null +++ b/test/viral_spiral/score/room_test.exs @@ -0,0 +1,33 @@ +defmodule ViralSpiral.Score.RoomTest do + alias ViralSpiral.Score.Change + alias ViralSpiral.Score.Room + use ExUnit.Case + + setup_all do + room = %Room{chaos_countdown: 4} + %{room: room} + end + + test "change chaos countdown", %{room: room} do + new_room = Change.apply_change(room, offset: 5) + assert new_room.chaos_countdown == 9 + end + + describe "invalid changes" do + setup do + room = %Room{chaos_countdown: 2} + %{room: room} + end + + test "pass invalid offset in change description", %{room: room} do + new_room = Change.apply_change(room, offset: "hi") + assert new_room.chaos_countdown == 2 + end + + test "pass opts without required fields", %{room: room} do + assert_raise ArgumentError, fn -> + Change.apply_change(room, invalid: "random") + end + end + end +end