Skip to content

Commit 53f3823

Browse files
committed
Allow undo to reset game at beginning
1 parent e0e4394 commit 53f3823

File tree

5 files changed

+119
-32
lines changed

5 files changed

+119
-32
lines changed

TODO.md

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
- CD: https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
44
- Put everyone's discards in front of their hand (suggested by Mom)
5-
- 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).
65
- Bug: Undoing a win after a bot drew a tile hung bot (workaround: paused & unpaused bots)
76
- Shrink tile sizes on mobile
87
- Lobby not receiving bot player added

lib/mjw/core/game.ex

+40-17
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,18 @@ defmodule Mjw.Game do
4343
turn_seatno: 0,
4444
# Where the deal picking started from. Might be used to count points.
4545
dealpick_seatno: 0,
46-
# Number of times the player has been dealer (wins, draws, DQs all count)
46+
# Number of times the player has been dealer (draws, etc, also count)
4747
dealer_win_count: 0,
4848
event_log: [],
4949
undo_seatno: nil,
5050
undo_state: nil,
5151
pause_bots: false
5252

5353
@doc """
54-
Initialize a game with a random ID and a shuffled deck
54+
Initialize a game, defaulting to a random ID and a shuffled deck
5555
"""
56-
def new() do
57-
new(Uniq.UUID.uuid4())
58-
end
59-
60-
defp new(id) do
61-
%__MODULE__{
62-
id: id,
63-
deck: shuffled_deck()
64-
}
56+
def new(id \\ Uniq.UUID.uuid4()) do
57+
%__MODULE__{id: id, deck: shuffled_deck()}
6558
end
6659

6760
defp shuffled_deck() do
@@ -193,7 +186,8 @@ defmodule Mjw.Game do
193186

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

389383
game
384+
|> set_undo_state_if_first_discard()
390385
|> log_discard_event(seatno, tile)
391386
|> update_seat(seatno, &Mjw.Seat.remove_from_concealed(&1, tile))
392387
|> Map.merge(%{discards: [tile | game.discards], turn_state: :drawing})
@@ -722,17 +717,26 @@ defmodule Mjw.Game do
722717
update_seat(game, seatno, fn _seat_being_replaced -> seat end)
723718
end
724719

725-
def undo(%__MODULE__{undo_seatno: undo_seatno, undo_state: %__MODULE__{} = undo_state} = game)
726-
when undo_seatno != nil do
727-
player_name = player_name_at(game, undo_seatno)
720+
def undo(
721+
%__MODULE__{undo_seatno: undo_seatno, undo_state: %__MODULE__{} = undo_state} = game,
722+
player_seatno
723+
) do
724+
player_name = player_name_at(game, player_seatno)
725+
726+
log_event_message =
727+
if undo_seatno do
728+
"#{player_name} undid their action."
729+
else
730+
"#{player_name} reset the game."
731+
end
728732

729733
undo_state
730734
# event_log is preserved as a singleton at the top-level game
731735
|> Map.merge(%{event_log: game.event_log})
732736
|> merge_seats_for_undo(game)
733737
# just in case undoing a declared win
734738
|> clear_all_seat_win_attributes()
735-
|> log_event("#{player_name} undid their action.")
739+
|> log_event(log_event_message)
736740
end
737741

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

771+
@doc """
772+
A player can undo when they performed the last human action, OR if the
773+
undo_seatno is nil which indicates it's the beginning of the game and we can
774+
undo the initial bot discards
775+
"""
776+
def can_undo?(%__MODULE__{undo_seatno: undo_seatno, undo_state: undo_state}, seatno) do
777+
undo_state && (!undo_seatno || undo_seatno == seatno)
778+
end
779+
767780
@doc """
768781
Draw a tile from the deck, and temporarily hold it in the seat's peektile
769782
before deciding whether to keep or discard. Params enforce that this player
@@ -960,11 +973,21 @@ defmodule Mjw.Game do
960973
# When an undoable change is made, set the undo_state so that the player in
961974
# undo_seatno can possibly undo it. event_log is not saved in undo states
962975
# because it exists as a singleton at the top-level game.
963-
defp set_undo_state(%__MODULE__{} = game, undo_seatno) do
976+
defp set_undo_state(%__MODULE__{} = game, undo_seatno \\ nil) do
964977
undo_state = game |> Map.delete(:event_log)
965978
%{game | undo_seatno: undo_seatno, undo_state: undo_state}
966979
end
967980

981+
# When a bot discards first, set the initial undo_state so a human can Undo
982+
# to the beginning of the game to pick up a bot's discard they missed
983+
defp set_undo_state_if_first_discard(%__MODULE__{} = game) do
984+
if Enum.empty?(game.discards) do
985+
set_undo_state(game)
986+
else
987+
game
988+
end
989+
end
990+
968991
def seat_bot(%__MODULE__{} = game) do
969992
empty_seatno = Enum.find_index(game.seats, &Mjw.Seat.empty?/1)
970993

lib/mjw/core/seat.ex

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ defmodule Mjw.Seat do
2626
end
2727

2828
@bot_id "bot"
29-
3029
def seat_bot(%__MODULE__{} = seat, bot_name) do
31-
%{seat | player_id: @bot_id, player_name: bot_name}
30+
seat_player(seat, @bot_id, bot_name)
3231
end
3332

3433
def bot?(%__MODULE__{player_id: @bot_id}), do: true

lib/mjw_web/live/game_live/show.ex

+3-4
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,8 @@ defmodule MjwWeb.GameLive.Show do
132132
when socket.assigns.current_user_can_undo do
133133
game =
134134
socket.assigns.game
135-
|> Mjw.Game.undo()
136-
|> optionally_enqueue_bot_roll(socket)
137-
|> optionally_enqueue_bot_draw(socket)
135+
|> Mjw.Game.undo(socket.assigns.current_user_seatno)
136+
|> optionally_enqueue_all_bot_actions
138137

139138
socket = update_game(socket, game, :undo)
140139

@@ -912,7 +911,7 @@ defmodule MjwWeb.GameLive.Show do
912911
|> assign(:deck_remaining, length(game.deck))
913912
|> assign(:empty_seats_count, Mjw.Game.empty_seats_count(game))
914913
|> assign(:turn_player_name, Mjw.Game.turn_player_name(game))
915-
|> assign(:current_user_can_undo, game.undo_seatno == current_user_seatno)
914+
|> assign(:current_user_can_undo, Mjw.Game.can_undo?(game, current_user_seatno))
916915
|> assign(:bots_present, Mjw.Game.bots_present?(game))
917916
end
918917

test/mjw/core/game_test.exs

+75-8
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,7 @@ defmodule Mjw.GameTest do
13961396
}
13971397

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

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

14281428
expected_event_log = [
14291429
{"name3 undid their action.", nil},
@@ -1454,7 +1454,7 @@ defmodule Mjw.GameTest do
14541454
game =
14551455
orig_game
14561456
|> Mjw.Game.pong(1, ["dp-0"], "dp-0")
1457-
|> Mjw.Game.undo()
1457+
|> Mjw.Game.undo(1)
14581458

14591459
expected_event_log = [
14601460
{"name1 undid their action.", nil},
@@ -1487,7 +1487,7 @@ defmodule Mjw.GameTest do
14871487
orig_game
14881488
|> Mjw.Game.peek_deck_tile(3)
14891489
|> Mjw.Game.clear_peektile(3)
1490-
|> Mjw.Game.undo()
1490+
|> Mjw.Game.undo(3)
14911491

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

1522-
game = game |> Mjw.Game.undo()
1522+
game = game |> Mjw.Game.undo(3)
15231523

15241524
expected_event_log = [
15251525
{"name3 undid their action.", nil},
@@ -1550,7 +1550,7 @@ defmodule Mjw.GameTest do
15501550
game =
15511551
orig_game
15521552
|> Mjw.Game.declare_win_from_discards(1, "dp-0")
1553-
|> Mjw.Game.undo()
1553+
|> Mjw.Game.undo(1)
15541554

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

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

16171617
{:draw_discard, game} = Mjw.Game.bot_draw(game)
1618-
game = Mjw.Game.undo(game)
1618+
game = Mjw.Game.undo(game, 0)
16191619

16201620
expected_event_log = [
16211621
{"name0 undid their action.", nil},
@@ -2039,4 +2039,71 @@ defmodule Mjw.GameTest do
20392039
assert game.turn_state == :discarding
20402040
end
20412041
end
2042+
2043+
describe "can_undo?" do
2044+
test "is false for all players before the first discard" do
2045+
game =
2046+
Mjw.Game.new()
2047+
|> Mjw.Game.seat_player("id0", "name0")
2048+
|> Mjw.Game.seat_player("id1", "name1")
2049+
|> Mjw.Game.seat_player("id2", "name2")
2050+
|> Mjw.Game.seat_bot()
2051+
|> Mjw.Game.pick_random_available_wind(0)
2052+
|> Mjw.Game.pick_random_available_wind(1)
2053+
|> Mjw.Game.pick_random_available_wind(2)
2054+
|> Mjw.Game.pick_random_available_wind(3)
2055+
|> Mjw.Game.roll_dice_and_reseat_players()
2056+
|> Mjw.Game.roll_dice_and_deal()
2057+
2058+
refute Mjw.Game.can_undo?(game, 0)
2059+
refute Mjw.Game.can_undo?(game, 1)
2060+
refute Mjw.Game.can_undo?(game, 2)
2061+
refute Mjw.Game.can_undo?(game, 3)
2062+
end
2063+
2064+
test "is true for all human players after the first bot discard" do
2065+
{:ok, game} =
2066+
Mjw.Game.new()
2067+
|> Mjw.Game.seat_player("id0", "name0")
2068+
|> Mjw.Game.seat_player("id1", "name1")
2069+
|> Mjw.Game.seat_player("id2", "name2")
2070+
|> Mjw.Game.seat_bot()
2071+
|> Mjw.Game.pick_random_available_wind(0)
2072+
|> Mjw.Game.pick_random_available_wind(1)
2073+
|> Mjw.Game.pick_random_available_wind(2)
2074+
|> Mjw.Game.pick_random_available_wind(3)
2075+
|> Mjw.Game.roll_dice_and_reseat_players()
2076+
|> Mjw.Game.roll_dice_and_deal()
2077+
|> Map.merge(%{turn_seatno: 3})
2078+
|> Mjw.Game.bot_discard()
2079+
2080+
assert Mjw.Game.can_undo?(game, 0)
2081+
assert Mjw.Game.can_undo?(game, 1)
2082+
assert Mjw.Game.can_undo?(game, 2)
2083+
end
2084+
2085+
test "when undo_seatno is set, is true only for the undo_seatno player id" do
2086+
orig_game =
2087+
Mjw.Game.new()
2088+
|> Mjw.Game.seat_player("id0", "name0")
2089+
|> Mjw.Game.seat_player("id1", "name1")
2090+
|> Mjw.Game.seat_player("id2", "name2")
2091+
|> Mjw.Game.seat_bot()
2092+
|> Mjw.Game.pick_random_available_wind(0)
2093+
|> Mjw.Game.pick_random_available_wind(1)
2094+
|> Mjw.Game.pick_random_available_wind(2)
2095+
|> Mjw.Game.pick_random_available_wind(3)
2096+
|> Mjw.Game.roll_dice_and_reseat_players()
2097+
|> Mjw.Game.roll_dice_and_deal()
2098+
|> Map.merge(%{turn_seatno: 0})
2099+
2100+
tile = orig_game.seats |> Enum.at(0) |> Map.get(:concealed) |> Enum.random()
2101+
{:ok, game} = orig_game |> Mjw.Game.discard(0, tile)
2102+
2103+
assert Mjw.Game.can_undo?(game, 0)
2104+
refute Mjw.Game.can_undo?(game, 1)
2105+
refute Mjw.Game.can_undo?(game, 2)
2106+
refute Mjw.Game.can_undo?(game, 3)
2107+
end
2108+
end
20422109
end

0 commit comments

Comments
 (0)