diff --git a/lib/viral_spiral/room/engine_config.ex b/lib/viral_spiral/room/engine_config.ex index 7ee4bf0..0431796 100644 --- a/lib/viral_spiral/room/engine_config.ex +++ b/lib/viral_spiral/room/engine_config.ex @@ -2,7 +2,7 @@ defmodule ViralSpiral.Game.EngineConfig do @moduledoc """ Global configuration for the game engine. - Unlike its room specific counterpart `ViralSpiral.Room.RoomConfig`, this configuration is global to every game room created on this server. These configuration are loaded at compile time and don't change throughout the runtime of the engine. + Unlike its room specific counterpart `ViralSpiral.Room.State.Room`, this configuration is global to every game room created on this server. These configuration are loaded at compile time and don't change throughout the runtime of the engine. An imagined usecase of this module is to support spinning up varied instances of viral spiral with different characteristics. For instance, - Spinning up a server where only communities from :yellow and :red are available for gameplay. diff --git a/lib/viral_spiral/room/room_config.ex b/lib/viral_spiral/room/room_config.ex deleted file mode 100644 index 8b234ff..0000000 --- a/lib/viral_spiral/room/room_config.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule ViralSpiral.Room.RoomConfig do - @moduledoc """ - Room specific configuration for every game. - """ - alias ViralSpiral.Bias - alias ViralSpiral.Affinity - alias ViralSpiral.Room.RoomConfig - alias ViralSpiral.Game.EngineConfig - - defstruct affinities: [], communities: [], chaos_counter: 0, volatality: :medium - - @type t :: %__MODULE__{ - affinities: list(Affinity.target()), - communities: list(Bias.target()), - chaos_counter: integer(), - volatality: EngineConfig.volatility() - } - - def new(player_count) do - engine_config = %EngineConfig{} - - affinities = engine_config.affinities - total_affinities = length(affinities) - - two_affinities = - Stream.repeatedly(fn -> :rand.uniform(total_affinities - 1) end) |> Enum.take(2) - - room_affinities = Enum.map(two_affinities, &Enum.at(affinities, &1)) - - communities = engine_config.communities - total_communities = length(communities) - - room_communities = - case total_communities do - x when x <= 3 -> Enum.shuffle(communities) |> Enum.take(2) - _ -> communities - end - - %RoomConfig{ - affinities: room_affinities, - communities: room_communities, - chaos_counter: engine_config.chaos_counter, - volatality: engine_config.volatility - } - end -end diff --git a/lib/viral_spiral/room/state/player.ex b/lib/viral_spiral/room/state/player.ex index f6da5ff..ca151ac 100644 --- a/lib/viral_spiral/room/state/player.ex +++ b/lib/viral_spiral/room/state/player.ex @@ -10,7 +10,7 @@ defmodule ViralSpiral.Room.State.Player do } """ alias ViralSpiral.Room - alias ViralSpiral.Room.RoomConfig + alias ViralSpiral.Room.State.Room alias ViralSpiral.Room.State.Player.ActiveCardDoesNotExist alias ViralSpiral.Room.State.Player.DuplicateActiveCardException alias ViralSpiral.Room.State.Player @@ -39,8 +39,8 @@ defmodule ViralSpiral.Room.State.Player do active_cards: list(String.t()) } - @spec new(RoomConfig.t()) :: t() - def new(%RoomConfig{} = room_config) do + @spec new(Room.t()) :: t() + def new(%Room{} = room_config) do identity = Enum.shuffle(room_config.communities) |> Enum.at(0) bias_list = Enum.filter(room_config.communities, &(&1 != identity)) diff --git a/lib/viral_spiral/room/state/room.ex b/lib/viral_spiral/room/state/room.ex index 44f3c88..b42ad51 100644 --- a/lib/viral_spiral/room/state/room.ex +++ b/lib/viral_spiral/room/state/room.ex @@ -1,33 +1,73 @@ defmodule ViralSpiral.Room.State.Room do @moduledoc """ - ## Example - + Room specific configuration for every game. """ + alias ViralSpiral.Bias + alias ViralSpiral.Affinity alias ViralSpiral.Room.State.Room + alias ViralSpiral.Game.EngineConfig - defstruct chaos_countdown: 10, - id: "", - name: "", + defstruct affinities: [], + communities: [], + chaos_counter: nil, + volatality: :medium, + id: nil, + name: nil, state: :uninitialized @all_states [:uninitialized, :waiting_for_players, :running, :paused] @type states :: :uninitialized | :waiting_for_players | :running | :paused @type t :: %__MODULE__{ - chaos_countdown: integer(), + affinities: list(Affinity.target()), + communities: list(Bias.target()), + chaos_counter: integer(), + volatality: EngineConfig.volatility(), id: String.t(), name: String.t(), state: states() } - @doc """ - Create a new Room with default values. - """ - @spec new() :: t() + def new(player_count) do + engine_config = %EngineConfig{} + + affinities = engine_config.affinities + total_affinities = length(affinities) + + two_affinities = + Stream.repeatedly(fn -> :rand.uniform(total_affinities - 1) end) + |> Stream.dedup() + |> Enum.take(2) + + room_affinities = Enum.map(two_affinities, &Enum.at(affinities, &1)) + + communities = engine_config.communities + + room_communities = + case player_count do + x when x <= 3 -> Enum.shuffle(communities) |> Enum.take(2) + _ -> communities + end + + %Room{ + id: UXID.generate!(prefix: "room", size: :small), + state: :uninitialized, + affinities: room_affinities, + communities: room_communities, + chaos_counter: engine_config.chaos_counter, + volatality: engine_config.volatility + } + end + def new() do + engine_config = %EngineConfig{} + %Room{ id: UXID.generate!(prefix: "room", size: :small), - state: :uninitialized + name: name(), + state: :uninitialized, + chaos_counter: engine_config.chaos_counter, + volatality: engine_config.volatility } end @@ -40,7 +80,86 @@ defmodule ViralSpiral.Room.State.Room do """ @spec countdown(t()) :: t() def countdown(%Room{} = room) do - %{room | chaos_countdown: room.chaos_countdown - 1} + %{room | chaos_counter: room.chaos_counter - 1} + end + + def name() do + adjectives = [ + "ambitious", + "basic", + "careful", + "dark", + "eager", + "fab", + "glib", + "happy", + "inept", + "jolly", + "keen", + "lavish", + "magic", + "neat", + "official", + "perfect", + "quack", + "rare", + "sassy", + "tall", + "velvet", + "weak" + ] + + nouns = [ + "apple", + "ball", + "cat", + "dog", + "eel", + "fish", + "goat", + "hen", + "island", + "joker", + "lion", + "monk", + "nose", + "oven", + "parrot", + "queen", + "rat", + "sun", + "tower", + "umbrella", + "venus", + "water", + "zebra" + ] + + Enum.random(adjectives) <> "-" <> Enum.random(nouns) + end + + def start(%Room{} = room, player_count) do + engine_config = %EngineConfig{} + + affinities = engine_config.affinities + total_affinities = length(affinities) + + two_affinities = + Stream.repeatedly(fn -> :rand.uniform(total_affinities - 1) end) + |> Stream.dedup() + |> Enum.take(2) + + room_affinities = Enum.map(two_affinities, &Enum.at(affinities, &1)) + + communities = engine_config.communities + + room_communities = + case player_count do + x when x <= 3 -> Enum.shuffle(communities) |> Enum.take(2) + _ -> communities + end + + %{room | affinities: room_affinities, communities: room_communities, state: :running} end end @@ -53,14 +172,10 @@ defimpl ViralSpiral.Room.State.Change, for: ViralSpiral.Room.State.Room do """ @spec apply_change(Room.t(), State.t(), keyword()) :: Room.t() def apply_change(%Room{} = score, _global_state, 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]) + opts = Keyword.validate!(opts, type: nil, offset: 0) - y when is_bitstring(y) -> - Map.put(score, :chaos_countdown, score.chaos_countdown) + case opts[:type] do + :chaos_coutdown -> Map.put(score, :chaos_couter, score.chaos_counter + opts[:offset]) end end end diff --git a/lib/viral_spiral/room/state/root.ex b/lib/viral_spiral/room/state/root.ex index 3b95915..f94b0a0 100644 --- a/lib/viral_spiral/room/state/root.ex +++ b/lib/viral_spiral/room/state/root.ex @@ -9,7 +9,7 @@ defmodule ViralSpiral.Room.State.Root 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.Room.RoomConfig + alias ViralSpiral.Room.State.Room alias ViralSpiral.Room.State.Turn alias ViralSpiral.Canon.Deck alias ViralSpiral.Room.State.Round @@ -18,22 +18,37 @@ defmodule ViralSpiral.Room.State.Root do alias ViralSpiral.Room.State.Root alias ViralSpiral.Room.State.Change - defstruct room_config: nil, - room: nil, + defstruct room: nil, players: [], round: nil, turn: nil, deck: nil @type t :: %__MODULE__{ - room_config: RoomConfig.t(), room: Room.t(), players: list(Player.t()), - round: Round.t() - # turn: Turn.t(), + round: Round.t(), + turn: Turn.t() # deck: Deck.t() } + def new(%Room{} = room, player_names) when is_list(player_names) do + players = + player_names + |> Enum.map(fn player_name -> Player.new(room) |> Player.set_name(player_name) end) + |> Enum.reduce(%{}, fn player, acc -> Map.put(acc, player.id, player) end) + + round = Round.new(players) + turn = Turn.new(round) + + %Root{ + room: room, + players: players, + round: round, + turn: turn + } + end + def set_round(%Root{} = game, round) do %{game | round: round} end @@ -46,8 +61,6 @@ defmodule ViralSpiral.Room.State.Root do # list({:ok, message :: String.t()} | {:error, reason :: String.t()}) def apply_changes(state, changes) do Enum.reduce(changes, state, fn change, state -> - # require IEx - # IEx.pry() data = get_target(state, elem(change, 0)) change_inst = elem(change, 1) new_value = apply_change(data, state, change_inst) diff --git a/lib/viral_spiral/room/state/round.ex b/lib/viral_spiral/room/state/round.ex index 7ea3912..dd7b65d 100644 --- a/lib/viral_spiral/room/state/round.ex +++ b/lib/viral_spiral/room/state/round.ex @@ -31,11 +31,17 @@ defmodule ViralSpiral.Room.State.Round do @type change_opts :: [type: :next] + @typedoc "Description of which player to skip and in which round" + @type skip :: %{ + player: String.t(), + round: :current | :next + } + @type t :: %__MODULE__{ order: list(String.t()), count: integer(), current: integer(), - skip: boolean() + skip: skip() | nil } @doc """ diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index fe27fbe..ed9ac26 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -1,5 +1,5 @@ defmodule Fixtures do - alias ViralSpiral.Room.RoomConfig + alias ViralSpiral.Room.State.Room alias ViralSpiral.Room.State.Player alias ViralSpiral.Deck.Card alias ViralSpiral.Room.State.Turn @@ -10,13 +10,13 @@ defmodule Fixtures do alias ViralSpiral.Room.State.Root def initialized_game() do - room_config = RoomConfig.new(4) + room = Room.new(4) player_list = [ - Player.new(room_config) |> Player.set_name("adhiraj"), - Player.new(room_config) |> Player.set_name("aman"), - Player.new(room_config) |> Player.set_name("krys"), - Player.new(room_config) |> Player.set_name("farah") + Player.new(room) |> Player.set_name("adhiraj"), + Player.new(room) |> Player.set_name("aman"), + Player.new(room) |> Player.set_name("krys"), + Player.new(room) |> Player.set_name("farah") ] players = Enum.reduce(player_list, %{}, fn player, acc -> Map.put(acc, player.id, player) end) @@ -25,8 +25,7 @@ defmodule Fixtures do turn = Turn.new(round) %Root{ - room_config: room_config, - room: Room.new(), + room: room, players: players, round: round, turn: turn @@ -34,7 +33,7 @@ defmodule Fixtures do end def players() do - room_config = %RoomConfig{} + room_config = %Room{} player_list = [ Player.new(room_config) |> Player.set_name("adhiraj"), diff --git a/test/viral_spiral/gameplay_test.exs b/test/viral_spiral/gameplay_test.exs index 61d4b07..32d46ab 100644 --- a/test/viral_spiral/gameplay_test.exs +++ b/test/viral_spiral/gameplay_test.exs @@ -1,14 +1,54 @@ defmodule ViralSpiral.GameTest do + @moduledoc """ + A step by step recreation of every step of a game. + + We only do 2 rounds for brevity. + """ + alias ViralSpiral.Room.State.Player + alias ViralSpiral.Room.State.Turn + alias ViralSpiral.Room.State.Round + alias ViralSpiral.Room.State.Root + alias ViralSpiral.Room.State.Room alias ViralSpiral.Game use ExUnit.Case - describe "card actions" do - setup do - game_state = Fixtures.initialized_game() - %{state: game_state} - end + test "Play a round" do + :rand.seed(:exsss, {1, 87, 90}) + + # Player One creates a room and gets its link to share with others + room = Room.new() + assert room.chaos_counter != nil + assert room.id != nil + assert room.state == :uninitialized + assert room.name != nil + assert room.name == "basic-venus" + + # Start a game with 4 Players + room = Room.start(room, 4) + assert room.affinities != [] + assert room.communities != [] + assert room.state == :running + + root = Root.new(room, ["adhiraj", "krys", "aman", "farah"]) + assert match?(%Root{players: %{}, round: %Round{}, turn: %Turn{}}, root) + + round = root.round + turn = root.turn + + player_a = Enum.at(round.order, 0) + player_b = Enum.at(round.order, 1) + player_c = Enum.at(round.order, 2) + player_d = Enum.at(round.order, 3) - test "passing an affinity card changes the player's clout and affinity", %{state: game_state} do - end + # draw a card and pass it and check state at every point + assert match?( + %Root{ + players: %{ + ^player_a => %Player{clout: 0}, + ^player_b => %Player{clout: 0, biases: %{yellow: 0}} + } + }, + root + ) end end diff --git a/test/viral_spiral/room/player_test.exs b/test/viral_spiral/room/player_test.exs index 9784432..ac5b0e9 100644 --- a/test/viral_spiral/room/player_test.exs +++ b/test/viral_spiral/room/player_test.exs @@ -1,5 +1,5 @@ defmodule ViralSpiral.Game.PlayerTest do - alias ViralSpiral.Room.RoomConfig + alias ViralSpiral.Room.State.Room alias ViralSpiral.Room.State.Player.ActiveCardDoesNotExist alias ViralSpiral.Room.State.Player.DuplicateActiveCardException alias ViralSpiral.Room.State.Player @@ -7,7 +7,7 @@ defmodule ViralSpiral.Game.PlayerTest do use ExUnit.Case test "create player from room config" do - room_config = RoomConfig.new(4) + room_config = Room.new(4) player = Player.new(room_config) diff --git a/test/viral_spiral/room/room_config_test.exs b/test/viral_spiral/room/room_config_test.exs deleted file mode 100644 index 5379889..0000000 --- a/test/viral_spiral/room/room_config_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -defmodule ViralSpiral.Room.RoomConfigTest do - alias ViralSpiral.Room.RoomConfig - use ExUnit.Case - - describe "deterministic room configs" do - test "communities - yellow, red; affinities - sock, houseboat" do - :rand.seed(:exsss, {1, 8, 12}) - room = RoomConfig.new(3) - - assert room == %RoomConfig{ - affinities: [:houseboat, :skub], - communities: [:yellow, :red], - chaos_counter: 10, - volatality: :medium - } - end - - test "communities - a,b; affinities - x,y" do - :rand.seed(:exsss, {1, 2, 12}) - room = RoomConfig.new(3) - - assert room = %RoomConfig{ - affinities: [:highfive, :skub], - communities: [:yellow, :blue], - chaos_counter: 10, - volatality: :medium - } - end - end -end diff --git a/test/viral_spiral/room/state/room_test.exs b/test/viral_spiral/room/state/room_test.exs index 612d3c4..07e7a0d 100644 --- a/test/viral_spiral/room/state/room_test.exs +++ b/test/viral_spiral/room/state/room_test.exs @@ -1,22 +1,54 @@ -defmodule ViralSpiral.Room.State.RoomTest do +defmodule ViralSpiral.Room.RoomTest do alias ViralSpiral.Room.State.Change alias ViralSpiral.Room.State.Room use ExUnit.Case + describe "deterministic room configs" do + test "communities - yellow, red; affinities - sock, houseboat" do + :rand.seed(:exsss, {1, 8, 12}) + room = Room.new(3) + + assert room == %Room{ + affinities: [:houseboat, :skub], + communities: [:yellow, :red], + chaos_counter: 10, + volatality: :medium + } + end + + test "communities - a,b; affinities - x,y" do + :rand.seed(:exsss, {1, 2, 12}) + room = Room.new(3) + + assert room = %Room{ + affinities: [:highfive, :skub], + communities: [:yellow, :blue], + chaos_counter: 10, + volatality: :medium + } + end + end + + describe "Room Functions" do + test "player one creates a room and collects its link" do + room = Room.name() |> IO.inspect() + end + end + describe "changes" do setup do - room = %Room{chaos_countdown: 4} + room = %Room{chaos_counter: 4} %{room: room} end test "change chaos countdown", %{room: room} do new_room = Change.apply_change(room, nil, offset: 5) - assert new_room.chaos_countdown == 9 + assert new_room.chaos_counter == 9 end test "pass invalid offset in change description", %{room: room} do new_room = Change.apply_change(room, nil, offset: "hi") - assert new_room.chaos_countdown == 4 + assert new_room.chaos_counter == 4 end test "pass opts without required fields", %{room: room} do