我正在尝试使用 Queue 在 Elixir 中实施 war game
这是游戏简介:
游戏从一副洗好的牌开始。套牌将被传递到您已经洗牌的程序中(详情如下)。牌以交替方式发给每个玩家,因此每个玩家有 26 张牌。 在每一轮中,两位玩家都会展示他们堆中的顶牌。拥有较高牌(按排名)的玩家赢得两张牌,将它们放在牌堆的底部。 A 被认为是高牌,这意味着卡片按升序排列为 2-10、Jack、Queen、King、Ace。 如果公开的牌是平的,就会发生战争!每个玩家翻开一张牌面朝下,然后再翻开一张牌面朝上。牌面朝上的玩家拿走两堆牌(六张牌——原来并列的两张牌,加上战争中的四张牌)。如果翻开的牌的等级再次相同,则每位玩家将另一张牌面朝下放置,然后将另一张牌翻面朝上。牌大的玩家拿走全部 10 张牌,依此类推。 当一个玩家用完牌时,他们是输家,另一个人是赢家。如果在一场战争中,玩家用完了牌,这也算输。
这是我的队列实现:
defmodule Queue do
use GenServer
@intial_state %{
size: 0,
queue: [],
}
# Client - Public and high level API
def start_link do
GenServer.start_link(__MODULE__, @intial_state)
end
def enqueue(pid, elems) do
GenServer.cast(pid, {:enqueue, elems})
end
def dequeue(pid) do
GenServer.call(pid, :dequeue)
end
def dequeue(pid, many) do
GenServer.call(pid, {:dequeue, many})
end
def size(pid) do
GenServer.call(pid, :size)
end
def front(pid) do
GenServer.call(pid, :front)
end
def rear(pid) do
GenServer.call(pid, :rear)
end
def flush(pid) do
GenServer.call(pid, :flush)
end
# Server - Public but internal API
# handle_cast - handle the demand asynchronously
# handle_call - handle the demand eagerly
@impl true
def init(init) do
{:ok, init}
end
@impl true
def handle_call(:size, _from, state) do
{:reply, state.size, state}
end
def handle_call(:front, _from, state) do
%{queue: xs} = state
case xs do
[] -> {:reply, nil, state}
[x | _] -> {:reply, x, state}
end
end
def handle_call(:rear, _from, state) do
%{queue: xs} = state
case xs do
[] -> {:reply, nil, state}
xs -> {:reply, List.last(xs), state}
end
end
def handle_call(:flush, _from, state) do
{elems, state} = deq_many(state, state.size)
{:reply, elems, state}
end
def handle_call(:dequeue, _from, state) do
{elem, state} = deq(state)
{:reply, elem, state}
end
def handle_call({:dequeue, many}, _from, state) do
{elems, state} = deq_many(state, many)
{:reply, Enum.reverse(elems), state}
end
defp deq_many(state, n) do
Enum.reduce(1..n, {[], state}, fn
_, {elems, state} ->
{elem, state} = deq(state)
{[elem | elems], state}
end)
end
defp deq(state) do
%{queue: xs, size: size} = state
case xs do
[] -> {nil, state}
[x | xs] -> {x, %{state | queue: xs, size: size - 1}}
end
end
@impl true
def handle_cast({:enqueue, elems}, state) when is_list(elems) do
%{queue: xs, size: x} = state
xs = List.foldr(elems, xs, &[&1 | &2])
{:noreply,
%{state | size: x + length(elems), queue: xs}}
end
def handle_cast({:enqueue, elem}, state) do
%{queue: xs, size: x} = state
{:noreply, %{state | size: x + 1, queue: Enum.reverse([elem | xs])}}
end
end
这是我目前对战争游戏的实现(超时失败):
defmodule War do
@doc """
The main module to the challenge.
This module exposes a deal/1 function to play the game.
You can run all tests executing `elixir war.ex`.
"""
require Integer
# prefers to use a weight as we don't represent the cards
@ace_weight 14
def deal(deck) do
{deck_1, deck_2} = deal_deck(deck)
{:ok, player_1} = Queue.start_link()
{:ok, player_2} = Queue.start_link()
:ok = Queue.enqueue(player_1, deck_1)
:ok = Queue.enqueue(player_2, deck_2)
winner = play_game(player_1, player_2)
Queue.size(winner)
|> then(&Queue.dequeue(winner, &1))
|> Enum.map(&remove_ace_weight/1)
end
defp deal_deck(deck) do
List.foldr(deck, {[], []}, &deal_player/2)
end
defp deal_player(card, {p1, p2}) do
if length(p1) == length(p2) do
{[apply_ace_weight(card) | p1], p2}
else
{p1, [apply_ace_weight(card) | p2]}
end
end
defp play_game(p1, p2) do
if winner = maybe_get_winner(p1, p2) do
winner
else
{turn_winner, cards} = play_turn(p1 ,p2)
push_cards(turn_winner, cards)
play_game(p1, p2)
end
end
defp play_turn(p1, p2, x \\ nil, y \\ nil, tied \\ []) do
x = x || Queue.dequeue(p1)
y = y || Queue.dequeue(p2)
cards = [x, y]
cond do
x > y -> {p1, cards ++ tied}
x < y -> {p2, cards ++ tied}
x == y -> war(p1, p2, cards ++ tied)
end
end
defp war(p1, p2, tied) do
cond do
!able_to_war?(p1) ->
cards = Queue.flush(p1) ++ Queue.flush(p2) ++ tied
{p2, cards}
!able_to_war?(p2) ->
cards = Queue.flush(p1) ++ Queue.flush(p2) ++ tied
{p1, cards}
true ->
face_down = [Queue.dequeue(p1), Queue.dequeue(p2)]
[next_x, next_y] = [Queue.dequeue(p1), Queue.dequeue(p2)]
play_turn(p1, p2, next_x, next_y, tied ++ face_down)
end
end
defp able_to_war?(player) do
Queue.size(player) > 3
end
# The game ends when a player losses all their cards
# so their Stack is empty
defp maybe_get_winner(player_1, player_2) do
cond do
Queue.size(player_1) == 0 -> player_2
Queue.size(player_2) == 0 -> player_1
true -> nil
end
end
defp apply_ace_weight(card) do
(card == 1 && @ace_weight) || card
end
defp remove_ace_weight(card) do
(card == @ace_weight && 1) || card
end
# Cards won from a war needs to be pushed in descending order
defp push_cards(player, cards) do
cards = Enum.sort(cards, :desc)
Queue.enqueue(player, cards)
end
end
这些是测试用例:
defmodule WarTest do
use ExUnit.Case
describe "War" do
test "deal_1" do
t1 = [1,1,1,1,13,13,13,13,11,11,11,11,12,12,12,12,10,10,10,10,9,9,9,9,7,7,7,7,8,8,8,8,6,6,6,6,5,5,5,5,4,4,4,4,3,3,3,3,2,2,2,2]
r1 = [1,1,1,1,13,13,13,13,12,12,12,12,11,11,11,11,10,10,10,10,9,9,9,9,8,8,8,8,7,7,7,7,6,6,6,6,5,5,5,5,4,4,4,4,3,3,3,3,2,2,2,2]
assert War.deal(t1) == r1
end
test "deal_2" do
t2 = [1,13,1,13,1,13,1,13,12,11,12,11,12,11,12,11,10,9,10,9,10,9,10,9,8,7,8,7,8,7,8,7,6,5,6,5,6,5,6,5,4,3,4,3,4,3,4,3,2,2,2,2]
r2 = [4,3,2,2,2,2,4,3,4,3,4,3,6,5,6,5,6,5,6,5,8,7,8,7,8,7,8,7,10,9,10,9,10,9,10,9,12,11,12,11,12,11,12,11,1,13,1,13,1,13,1,13]
assert War.deal(t2) == r2
end
test "deal_3" do
t3 = [13,1,13,1,13,1,13,1,11,12,11,12,11,12,11,12,9,10,9,10,9,10,9,10,7,8,7,8,7,8,7,8,5,6,5,6,5,6,5,6,3,4,3,4,3,4,3,4,2,2,2,2]
r3 = [4,3,2,2,2,2,4,3,4,3,4,3,6,5,6,5,6,5,6,5,8,7,8,7,8,7,8,7,10,9,10,9,10,9,10,9,12,11,12,11,12,11,12,11,1,13,1,13,1,13,1,13]
assert War.deal(t3) == r3
end
test "deal_4" do
t4 = [10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9]
r4 = [1,1,13,12,9,5,11,4,9,3,8,7,7,2,13,10,12,5,10,4,9,6,8,3,1,1,13,12,7,5,11,4,9,3,8,6,7,2,13,10,12,5,11,11,10,8,6,4,6,3,2,2]
assert War.deal(t4) == r4
end
test "deal_5" do
t5 = [1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13,1,2,3,4,5,6,7,8,9,10,11,12,13]
r5 = [1,10,13,8,11,9,8,7,11,8,13,7,13,6,12,6,9,5,8,5,7,4,7,4,11,6,12,10,6,3,2,2,12,5,9,3,10,4,9,2,10,3,5,2,1,1,1,13,12,11,4,3]
assert War.deal(t5) == r5
end
defp create_deck do
deck =
for n <- 1..13, _ <- 1..4 do
n
end
Enum.shuffle(deck)
end
test "should return the same number of cards after deal" do
deck = create_deck()
assert length(deck) == 52
assert length(War.deal(deck)) == 52
end
test "should remove the ace weight after a win" do
deck = create_deck()
assert Enum.all?(War.deal(deck), &(&1 != 14))
end
end
end
我还公开了这个挑战的回购:https://github.com/zoedsoupe/war.ex
我试图理解为什么我的代码在第一个测试用例中成功,但在其他测试用例中失败,以及我如何改进它以达到预期的结果。
首先,并非所有洗牌后的牌组都有赢家。考虑一副由 2 个花色组成的牌组,每个花色有 2 个等级(1♠️、2♠️、1❤、2❤。)将其洗牌为
{[1♠️, 2♠️], [2❤, 1❤]}
,看看循环如何变成无限。这同样适用于任何有偶数张牌的牌组,但不失一般性。
另外,在战争中,当双方玩家同时用完牌时,可能会出现平局。
最后但并非最不重要的一点是,必须有一个规则,即获胜者的半副牌堆中的堆叠顺序。
就是说,如果已经看到特定组合,我将从检测
Queue
实现中的循环开始,以声明绘制。
此外,我会避免尝试使用两个列表,这肯定是过早优化的标志,我怀疑它是否有意义。