Skip to content

Commit

Permalink
Checkpoint.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpearce committed Oct 27, 2023
1 parent e89f353 commit 4a0ac2e
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 41 deletions.
18 changes: 15 additions & 3 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ h1 {
color: var(--text-color);
}

a {
h1 a {
position: relative;
text-decoration: none;
}

h1 a:hover {
text-decoration: none;
}

/* Animate a solid underline sliding in and out on hover. */
a::after {
h1 a::after {
content: '';
position: absolute;
width: 100%;
Expand All @@ -69,11 +73,19 @@ a::after {
transition: transform 0.25s ease-out;
}

a:hover::after {
h1 a:hover::after {
transform: scaleX(1);
transform-origin: bottom left;
}

a {
color: var(--text-color);
}

a:hover {
text-decoration: underline;
}

.cozy {
padding: 8px;
background-color: #fbcfe8;
Expand Down
77 changes: 53 additions & 24 deletions lib/twenty_fourty_eight/game/engine.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule TwentyFourtyEight.Game.Engine do
@default_options [
# TODO obstacles
@default_new_options [
# Number of cells per row and per column.
board_dimensions: {6, 6},
# Value of the singular piece present at the beginning of the game.
Expand All @@ -10,26 +11,34 @@ defmodule TwentyFourtyEight.Game.Engine do
# Value of the piece which, when present on the board, results in a win.
winning_number: 2048
]
@all_options Keyword.keys(@default_new_options) ++ [:board, :score, :turns, :state]
@valid_moves [:up, :down, :left, :right]

def init(opts \\ []) do
opts = Keyword.validate!(opts, @default_options)

# TODO validate that value options are all powers of two and that starting
# and turn start values are both less than winning value.

%{
board: starting_board(opts[:board_dimensions], opts[:starting_number]),
def init(%{state: :new} = opts) do
opts
# |> Keyword.validate!(@default_new_options)
|> Map.merge(%{
board: starting_board({opts.num_rows, opts.num_cols}, opts.starting_number),
score: 0,
turns: 0,
state: :running,
turn_start_number: opts[:turn_start_number],
winning_number: opts[:winning_number]
state: :running
})
|> init()
end

def init(opts) do
%{
board: opts.board,
score: opts.score,
turns: opts.turns,
state: opts.state,
turn_start_number: opts.turn_start_number,
winning_number: opts.winning_number
}
end

def tick(%{state: :running} = game, move) when move in @valid_moves do
update(game, move)
apply_move(game, move)
end

defp starting_board({num_rows, num_cols}, starting_number) do
Expand Down Expand Up @@ -57,17 +66,37 @@ defmodule TwentyFourtyEight.Game.Engine do
|> Enum.any?(fn value -> value == winning_number end)
end

defp update(
%{board: board, turns: turns, turn_start_number: turn_start_number, state: :running} =
game,
move
) do
# TODO Increase score (by the sum of newly merged pieces).
board = merge_values(board, move)
board = move_values(board, move)
# TODO only increment turns and check for wins/exhaustion if the move
# actually modified the board.
game = %{game | board: board, turns: turns + 1}
defp apply_move(%{board: board} = game, move) do
updated_board = board |> merge_values(move) |> move_values(move)

# Only need to update state if the board changed.
if Map.equal?(board, updated_board) do
game
else
turn_score = compute_score(board, updated_board)
apply_turn(game, updated_board, turn_score)
end
end

defp compute_score(%{cells: cells_before} = _board_before, %{cells: cells_after} = _board_after) do
value_counts_before = cells_before |> Map.values() |> Enum.frequencies()
value_counts_after = cells_after |> Map.values() |> Enum.frequencies()

# Any values not present after a move must be due to merges.
# Credit merges as the sum of all disappearing values.
value_counts_before
|> Enum.map(fn
{nil, _count} -> 0

{value, count} ->
difference = count - Map.get(value_counts_after, value, 0)
if difference > 0, do: value * difference, else: 0
end)
|> Enum.sum()
end

defp apply_turn(%{score: score, turns: turns, turn_start_number: turn_start_number} = game, board, turn_score) do
game = %{game | board: board, turns: turns + 1, score: score + turn_score}

if won?(game) do
%{game | state: :won}
Expand Down
4 changes: 4 additions & 0 deletions lib/twenty_fourty_eight/game/game.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ defmodule TwentyFourtyEight.Game.Game do
field :turn_start_number, :integer, default: 1
field :winning_number, :integer, default: 2048
field :slug, :string
field :score, :integer
field :turns, :integer
field :state, Ecto.Enum, values: [:new, :running, :won, :exhausted]
field :board, :map
timestamps()
end

Expand Down
13 changes: 6 additions & 7 deletions lib/twenty_fourty_eight/game/manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ defmodule TwentyFourtyEight.Game.Manager do
@registry TwentyFourtyEight.Game.Registry
@supervisor TwentyFourtyEight.Game.Supervisor

def get_game(name, _state) when is_binary(name) do
def get_game(name, state) when is_binary(name) do
case Registry.lookup(@registry, name) do
[{pid, _value}] -> {:ok, pid}
[] -> DynamicSupervisor.start_child(@supervisor, {__MODULE__, name})
[] -> DynamicSupervisor.start_child(@supervisor, {__MODULE__, {name, state}})
end
end

Expand All @@ -21,15 +21,14 @@ defmodule TwentyFourtyEight.Game.Manager do
GenServer.call(via_tuple(name), :state)
end

def start_link(name) do
GenServer.start_link(__MODULE__, [name: name], name: via_tuple(name))
def start_link({name, state}) do
GenServer.start_link(__MODULE__, state, name: via_tuple(name))
end

@impl true
def init(name: name) do
IO.puts("Starting manager #{name}")
def init(state) do
Process.flag(:trap_exit, true)
{:ok, Engine.init()}
{:ok, Engine.init(state)}
end

@impl true
Expand Down
23 changes: 16 additions & 7 deletions lib/twenty_fourty_eight_web/live/game_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ defmodule TwentyFourtyEightWeb.GameLive do
alias TwentyFourtyEight.Game.Game
alias TwentyFourtyEight.Game.Manager, as: GameManager

# Support arrows keys as well as hjkl (Vim) and wasd (gaming).
@up_keys ["ArrowUp", "w", "k"]
@down_keys ["ArrowDown", "s", "j"]
@left_keys ["ArrowLeft", "a", "h"]
@right_keys ["ArrowRight", "d", "l"]
# Support arrows keys, hjkl, and wasd for movement.
@up_keys ~w(ArrowUp w k)
@down_keys ~w(ArrowDown s j)
@left_keys ~w(ArrowLeft a h)
@right_keys ~w(ArrowRight d l)
@known_keys @up_keys ++ @down_keys ++ @left_keys ++ @right_keys

def render(assigns) do
~H"""
<div class="cozy">
<div class="stats">
<div><b>Name</b> <code><%= @name %></code></div>
<div><b>Name</b> <a href={~p"/#{@name}"}><code><%= @name %></code></a></div>
<div><b>Score</b> <%= @score %></div>
<div><b>Turns</b> <%= @turns %></div>
</div>
Expand All @@ -37,6 +37,10 @@ defmodule TwentyFourtyEightWeb.GameLive do
# TODO change game to die after no interaction (or put similar logic in the manager?)
{:ok, _pid} = GameManager.get_game(name, game)

if connected?(socket) do
Phoenix.PubSub.subscribe(TwentyFourtyEight.PubSub, name)
end

{:ok, assign_game(socket, name)}
end
end
Expand All @@ -47,11 +51,16 @@ defmodule TwentyFourtyEightWeb.GameLive do
def handle_event("move", %{"key" => key}, %{assigns: %{name: name, state: :running}} = socket)
when key in @known_keys do
:ok = GameManager.tick(name, key_to_move(key))
{:noreply, assign_game_state(socket, name)}
Phoenix.PubSub.broadcast(TwentyFourtyEight.PubSub, name, {:update, name})
{:noreply, socket}
end

def handle_event("move", _params, socket), do: {:noreply, socket}

def handle_info({:update, name}, socket) do
{:noreply, assign_game_state(socket, name)}
end

defp key_to_move(up) when up in @up_keys, do: :up
defp key_to_move(down) when down in @down_keys, do: :down
defp key_to_move(left) when left in @left_keys, do: :left
Expand Down

0 comments on commit 4a0ac2e

Please sign in to comment.