Skip to content

Commit

Permalink
Allow undo to reset game at beginning
Browse files Browse the repository at this point in the history
  • Loading branch information
m1foley committed Jul 4, 2024
1 parent e0e4394 commit 53f3823
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 32 deletions.
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

- CD: https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
- Put everyone's discards in front of their hand (suggested by Mom)
- If no undoable moves, Undo button should reset the game to get first discarded tile of the game from bot (human went last, missed a discarded tile, can't go back to beginning of game).
- Bug: Undoing a win after a bot drew a tile hung bot (workaround: paused & unpaused bots)
- Shrink tile sizes on mobile
- Lobby not receiving bot player added
Expand Down
57 changes: 40 additions & 17 deletions lib/mjw/core/game.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,18 @@ defmodule Mjw.Game do
turn_seatno: 0,
# Where the deal picking started from. Might be used to count points.
dealpick_seatno: 0,
# Number of times the player has been dealer (wins, draws, DQs all count)
# Number of times the player has been dealer (draws, etc, also count)
dealer_win_count: 0,
event_log: [],
undo_seatno: nil,
undo_state: nil,
pause_bots: false

@doc """
Initialize a game with a random ID and a shuffled deck
Initialize a game, defaulting to a random ID and a shuffled deck
"""
def new() do
new(Uniq.UUID.uuid4())
end

defp new(id) do
%__MODULE__{
id: id,
deck: shuffled_deck()
}
def new(id \\ Uniq.UUID.uuid4()) do
%__MODULE__{id: id, deck: shuffled_deck()}
end

defp shuffled_deck() do
Expand Down Expand Up @@ -193,7 +186,8 @@ defmodule Mjw.Game do

@doc """
Roll dice and deal the deck. The dealer will have 14 tiles and others will
have 13. Set dealpick_seatno and change turn_state to discarding.
have 13. Sets dealpick_seatno, changes turn_state to discarding, and sets
undo_state to an initial value.
"""
def roll_dice_and_deal(%__MODULE__{turn_state: :rolling} = game) do
game
Expand Down Expand Up @@ -387,6 +381,7 @@ defmodule Mjw.Game do
tile = Mjw.BotStrategy.discard(game)

game
|> set_undo_state_if_first_discard()
|> log_discard_event(seatno, tile)
|> update_seat(seatno, &Mjw.Seat.remove_from_concealed(&1, tile))
|> Map.merge(%{discards: [tile | game.discards], turn_state: :drawing})
Expand Down Expand Up @@ -722,17 +717,26 @@ defmodule Mjw.Game do
update_seat(game, seatno, fn _seat_being_replaced -> seat end)
end

def undo(%__MODULE__{undo_seatno: undo_seatno, undo_state: %__MODULE__{} = undo_state} = game)
when undo_seatno != nil do
player_name = player_name_at(game, undo_seatno)
def undo(
%__MODULE__{undo_seatno: undo_seatno, undo_state: %__MODULE__{} = undo_state} = game,
player_seatno
) do
player_name = player_name_at(game, player_seatno)

log_event_message =
if undo_seatno do
"#{player_name} undid their action."
else
"#{player_name} reset the game."
end

undo_state
# event_log is preserved as a singleton at the top-level game
|> Map.merge(%{event_log: game.event_log})
|> merge_seats_for_undo(game)
# just in case undoing a declared win
|> clear_all_seat_win_attributes()
|> log_event("#{player_name} undid their action.")
|> log_event(log_event_message)
end

# The undo player's hand will change after an undo, but try to preserve their
Expand Down Expand Up @@ -764,6 +768,15 @@ defmodule Mjw.Game do
end)
end

@doc """
A player can undo when they performed the last human action, OR if the
undo_seatno is nil which indicates it's the beginning of the game and we can
undo the initial bot discards
"""
def can_undo?(%__MODULE__{undo_seatno: undo_seatno, undo_state: undo_state}, seatno) do
undo_state && (!undo_seatno || undo_seatno == seatno)
end

@doc """
Draw a tile from the deck, and temporarily hold it in the seat's peektile
before deciding whether to keep or discard. Params enforce that this player
Expand Down Expand Up @@ -960,11 +973,21 @@ defmodule Mjw.Game do
# When an undoable change is made, set the undo_state so that the player in
# undo_seatno can possibly undo it. event_log is not saved in undo states
# because it exists as a singleton at the top-level game.
defp set_undo_state(%__MODULE__{} = game, undo_seatno) do
defp set_undo_state(%__MODULE__{} = game, undo_seatno \\ nil) do
undo_state = game |> Map.delete(:event_log)
%{game | undo_seatno: undo_seatno, undo_state: undo_state}
end

# When a bot discards first, set the initial undo_state so a human can Undo
# to the beginning of the game to pick up a bot's discard they missed
defp set_undo_state_if_first_discard(%__MODULE__{} = game) do
if Enum.empty?(game.discards) do
set_undo_state(game)
else
game
end
end

def seat_bot(%__MODULE__{} = game) do
empty_seatno = Enum.find_index(game.seats, &Mjw.Seat.empty?/1)

Expand Down
3 changes: 1 addition & 2 deletions lib/mjw/core/seat.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ defmodule Mjw.Seat do
end

@bot_id "bot"

def seat_bot(%__MODULE__{} = seat, bot_name) do
%{seat | player_id: @bot_id, player_name: bot_name}
seat_player(seat, @bot_id, bot_name)
end

def bot?(%__MODULE__{player_id: @bot_id}), do: true
Expand Down
7 changes: 3 additions & 4 deletions lib/mjw_web/live/game_live/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ defmodule MjwWeb.GameLive.Show do
when socket.assigns.current_user_can_undo do
game =
socket.assigns.game
|> Mjw.Game.undo()
|> optionally_enqueue_bot_roll(socket)
|> optionally_enqueue_bot_draw(socket)
|> Mjw.Game.undo(socket.assigns.current_user_seatno)
|> optionally_enqueue_all_bot_actions

socket = update_game(socket, game, :undo)

Expand Down Expand Up @@ -912,7 +911,7 @@ defmodule MjwWeb.GameLive.Show do
|> assign(:deck_remaining, length(game.deck))
|> assign(:empty_seats_count, Mjw.Game.empty_seats_count(game))
|> assign(:turn_player_name, Mjw.Game.turn_player_name(game))
|> assign(:current_user_can_undo, game.undo_seatno == current_user_seatno)
|> assign(:current_user_can_undo, Mjw.Game.can_undo?(game, current_user_seatno))
|> assign(:bots_present, Mjw.Game.bots_present?(game))
end

Expand Down
83 changes: 75 additions & 8 deletions test/mjw/core/game_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1396,7 +1396,7 @@ defmodule Mjw.GameTest do
}

{:ok, game} = Mjw.Game.discard(orig_game, 3, "n3-3")
game = Mjw.Game.undo(game)
game = Mjw.Game.undo(game, 3)

expected_event_log = [{"name3 undid their action.", nil}, {"name3 discarded.", "n3-3"}]
assert game == %{orig_game | event_log: expected_event_log}
Expand All @@ -1423,7 +1423,7 @@ defmodule Mjw.GameTest do
game =
orig_game
|> Mjw.Game.draw_discard(3, ["dp-0"], "dp-0")
|> Mjw.Game.undo()
|> Mjw.Game.undo(3)

expected_event_log = [
{"name3 undid their action.", nil},
Expand Down Expand Up @@ -1454,7 +1454,7 @@ defmodule Mjw.GameTest do
game =
orig_game
|> Mjw.Game.pong(1, ["dp-0"], "dp-0")
|> Mjw.Game.undo()
|> Mjw.Game.undo(1)

expected_event_log = [
{"name1 undid their action.", nil},
Expand Down Expand Up @@ -1487,7 +1487,7 @@ defmodule Mjw.GameTest do
orig_game
|> Mjw.Game.peek_deck_tile(3)
|> Mjw.Game.clear_peektile(3)
|> Mjw.Game.undo()
|> Mjw.Game.undo(3)

expected_event_log = [
{"name3 undid their action.", nil},
Expand Down Expand Up @@ -1519,7 +1519,7 @@ defmodule Mjw.GameTest do
{game, "b1-0"} =
orig_game |> Mjw.Game.draw_correction_tile(3, ["n1-3", "n2-3", "n3-3", "decktile"])

game = game |> Mjw.Game.undo()
game = game |> Mjw.Game.undo(3)

expected_event_log = [
{"name3 undid their action.", nil},
Expand Down Expand Up @@ -1550,7 +1550,7 @@ defmodule Mjw.GameTest do
game =
orig_game
|> Mjw.Game.declare_win_from_discards(1, "dp-0")
|> Mjw.Game.undo()
|> Mjw.Game.undo(1)

expected_event_log = [{"name1 undid their action.", nil}, {"name1 went out!", "dp-0"}]
assert game == %{orig_game | event_log: expected_event_log}
Expand All @@ -1577,7 +1577,7 @@ defmodule Mjw.GameTest do
game =
orig_game
|> Mjw.Game.declare_win_from_hand(1, "n3-1")
|> Mjw.Game.undo()
|> Mjw.Game.undo(1)

expected_event_log = [{"name1 undid their action.", nil}, {"name1 went out!", "n3-1"}]
assert game == %{orig_game | event_log: expected_event_log}
Expand Down Expand Up @@ -1615,7 +1615,7 @@ defmodule Mjw.GameTest do
{:ok, game} = Mjw.Game.discard(orig_game, 0, "b5-0")

{:draw_discard, game} = Mjw.Game.bot_draw(game)
game = Mjw.Game.undo(game)
game = Mjw.Game.undo(game, 0)

expected_event_log = [
{"name0 undid their action.", nil},
Expand Down Expand Up @@ -2039,4 +2039,71 @@ defmodule Mjw.GameTest do
assert game.turn_state == :discarding
end
end

describe "can_undo?" do
test "is false for all players before the first discard" do
game =
Mjw.Game.new()
|> Mjw.Game.seat_player("id0", "name0")
|> Mjw.Game.seat_player("id1", "name1")
|> Mjw.Game.seat_player("id2", "name2")
|> Mjw.Game.seat_bot()
|> Mjw.Game.pick_random_available_wind(0)
|> Mjw.Game.pick_random_available_wind(1)
|> Mjw.Game.pick_random_available_wind(2)
|> Mjw.Game.pick_random_available_wind(3)
|> Mjw.Game.roll_dice_and_reseat_players()
|> Mjw.Game.roll_dice_and_deal()

refute Mjw.Game.can_undo?(game, 0)
refute Mjw.Game.can_undo?(game, 1)
refute Mjw.Game.can_undo?(game, 2)
refute Mjw.Game.can_undo?(game, 3)
end

test "is true for all human players after the first bot discard" do
{:ok, game} =
Mjw.Game.new()
|> Mjw.Game.seat_player("id0", "name0")
|> Mjw.Game.seat_player("id1", "name1")
|> Mjw.Game.seat_player("id2", "name2")
|> Mjw.Game.seat_bot()
|> Mjw.Game.pick_random_available_wind(0)
|> Mjw.Game.pick_random_available_wind(1)
|> Mjw.Game.pick_random_available_wind(2)
|> Mjw.Game.pick_random_available_wind(3)
|> Mjw.Game.roll_dice_and_reseat_players()
|> Mjw.Game.roll_dice_and_deal()
|> Map.merge(%{turn_seatno: 3})
|> Mjw.Game.bot_discard()

assert Mjw.Game.can_undo?(game, 0)
assert Mjw.Game.can_undo?(game, 1)
assert Mjw.Game.can_undo?(game, 2)
end

test "when undo_seatno is set, is true only for the undo_seatno player id" do
orig_game =
Mjw.Game.new()
|> Mjw.Game.seat_player("id0", "name0")
|> Mjw.Game.seat_player("id1", "name1")
|> Mjw.Game.seat_player("id2", "name2")
|> Mjw.Game.seat_bot()
|> Mjw.Game.pick_random_available_wind(0)
|> Mjw.Game.pick_random_available_wind(1)
|> Mjw.Game.pick_random_available_wind(2)
|> Mjw.Game.pick_random_available_wind(3)
|> Mjw.Game.roll_dice_and_reseat_players()
|> Mjw.Game.roll_dice_and_deal()
|> Map.merge(%{turn_seatno: 0})

tile = orig_game.seats |> Enum.at(0) |> Map.get(:concealed) |> Enum.random()
{:ok, game} = orig_game |> Mjw.Game.discard(0, tile)

assert Mjw.Game.can_undo?(game, 0)
refute Mjw.Game.can_undo?(game, 1)
refute Mjw.Game.can_undo?(game, 2)
refute Mjw.Game.can_undo?(game, 3)
end
end
end

0 comments on commit 53f3823

Please sign in to comment.