Making a Cards Against Humanity with Elixir & Phoenix
Daven
Posted on December 26, 2021
When the pandemic started me and other friends are looking for a game to play online. One of the games we found was https://pyx-1.pretendyoure.xyz/zy/game.jsp which is a Cards Against Humanity clone written in Java. They did a really good job of implementing features like being able to pick card packs you want & in-game chat feature. The problem is that it's not mobile friendly so you'll have to play it through the desktop view.
I've been using this programming language called Elixir at my job and figured that it might be a good idea to write a CAH clone using it. So i created Leafblower
It has the following features
- In-game chat
- It's mobile friendly so you can play it on the go
- It currently uses the base CAH card pack
Show me the code!
It all starts by generating a game code. We call GameStatem.generate_game_code/1
to find an unused game code first. Then we call GameSupervisor.new_game
to start a game
leafblower_web/controllers/game_splash_live.ex
def handle_event("new_game", %{"user" => params}, socket) do
{:ok, code} = Leafblower.GameStatem.generate_game_code()
data =
cast_user(params)
|> Ecto.Changeset.apply_changes()
{:ok, game} =
Leafblower.GameSupervisor.new_game(id: code, countdown_duration: 120, min_player_count: 2)
Leafblower.GameStatem.join_player(game, socket.assigns.user_id, data.name)
{:noreply,
socket
|> push_redirect(to: Routes.live_path(socket, LeafblowerWeb.GameLive, code), replace: true)}
end
When the GameSupervisor.new_game/1
is called it will start 2 process:
- GameTicker - The process that handles countdown timers and publishes a tick message everytime it ticks
- GameStatem - This stores the current state of the game as well as additional data like player names and ids
def new_game(arg) do
{:ok, _} =
Horde.DynamicSupervisor.start_child(
__MODULE__,
{GameTicker, Keyword.take(arg, [:id])}
)
Horde.DynamicSupervisor.start_child(
__MODULE__,
{GameStatem, arg}
)
end
The GameStatem is where most of the magic happens. If you look at the file, you'll see the status
and data
typesecs
defmodule Leafblower.GameStatem do
@type player_id :: binary()
@type player_info :: %{player_id() => %{name: binary()}}
@type status :: :waiting_for_players | :round_started_waiting_for_response | :round_ended
@type data :: %{
id: binary(),
active_players: MapSet.t(player_id()),
player_info: player_info(),
round_number: non_neg_integer(),
round_player_answers: %{player_id() => list(binary())},
required_white_cards_count: non_neg_integer | nil,
leader_player_id: player_id() | nil,
winner_player_id: player_id() | nil,
....
}
When the game is started, the default status
will be :waiting_for_players
. This will change as the game progresses. The controller sends messages to the GameStatem
process and it will broadcast any changes to its state and data through an internal handle_event callback
def handle_event(:internal, :broadcast, status, data) do
Phoenix.PubSub.broadcast(
Leafblower.PubSub,
topic(data.id),
{:game_state_changed, status, Map.drop(data, [:deck])}
)
:keep_state_and_data
end
Closing thoughts
I had extreme fun building this game. Getting feedback from my friends who played was also very helpful. I hope you enjoy as well!
Posted on December 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024