bug(copi): highest_scoring_cards_in_game applies special_lead_cards across all rounds before grouping by round

オープン
#3,783 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
55/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
elixir
領域
backend

調査の方向性

Read lib/copi/cornucopia.ex, especially highest_scoring_cards_in_game/1 and special_lead_cards/1; the issue includes a two-round regression example for test/copi/cornucopia_logic_test.exs. Check how round grouping affects lead suit, trump, and joker filtering, then run the focused test. Done when each round returns its own highest scoring card and the regression tests pass.

索引モデルが issue の本文から書いたものです。

説明

Describe the bug

In copi.owasp.org/lib/copi/cornucopia.ex (lines 404–411), highest_scoring_cards_in_game/1 calls special_lead_cards/1 on the flat list of all played cards across the entire game before grouping cards by played_in_round:

def highest_scoring_cards_in_game(game) do
  Enum.reduce(game.players, [], fn player, cards -> (player.dealt_cards |> played_cards) ++ cards end) # Combine all played cards from all players
    |> lead_suit_cards # Filter to just the lead suit cards
    |> scoring_cards(Enum.count(game.players)) # Filter to just the cards that scored
    |> special_lead_cards # Filter to just the special lead cards (jokers, trump cards, or lead suit cards)
    |> Enum.group_by(fn card -> card.played_in_round end) # Convert to map where {round, [scoring_cards]}
    |> Enum.flat_map(fn {_, cards} -> highest_card(cards) end) # Back to a list of just the highest scoring card in each round
end

Because lead_suit_cards/1 flattens all rounds back into a single list via Enum.flat_map/2, special_lead_cards/1 (lines 431–457) evaluates jokers, trump suits, and lead_suit = List.first(ordered_played_cards).card.category across all rounds combined instead of per round:

  1. Different lead suits across rounds: If Round 1 is led with "Authentication" and Round 2 is led with "Cryptography" (with no jokers or trumps in either round), List.first(ordered_played_cards) in special_lead_cards/1 has category: "Authentication", so Enum.filter(ordered_played_cards, &(&1.card.category == lead_suit)) discards all "Cryptography" cards from Round 2.
  2. Trump card in one round wipes out non-trump rounds: If any card from a trump suit (Cornucopia, Dataset Risk, Elevation of Privilege) scores in Round 1, trumps != [] is true for the whole-game list and special_lead_cards/1 discards all non-trump scoring cards from every other round.
  3. Joker in one round wipes out non-joker rounds: If JokerA or JokerB scores in Round 1, jokers != [] is true for the whole-game list and special_lead_cards/1 discards all non-joker scoring cards from every other round.

As a result, highest_scoring_card_in_round(game, round) returns nil for affected rounds and highest_scoring_cards_for_player(player, game) omits the round winner's Highest Card Bonus point on the scoreboard (CopiWeb.GameComponents.game_scores/1).

Proof / Reproduction

In copi.owasp.org/test/copi/cornucopia_logic_test.exs, existing tests only play cards in round 1. Playing two rounds with different lead suits reproduces the bug:

test "highest_scoring_card_in_round evaluates each round independently", %{game: game, p1: p1, p2: p2} do
  {:ok, r1_c1} = create_card("Authentication", "K")
  {:ok, r1_c2} = create_card("Authentication", "5")
  {:ok, r2_c1} = create_card("Cryptography", "Q")
  {:ok, r2_c2} = create_card("Cryptography", "3")

  # Round 1: Authentication lead suit
  d1 = play_card(p1, r1_c1, 1)
  d2 = play_card(p2, r1_c2, 1)
  Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d1.id, player_id: p2.id})
  Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d2.id, player_id: p1.id})

  # Round 2: Cryptography lead suit
  d3 = play_card(p2, r2_c1, 2)
  d4 = play_card(p1, r2_c2, 2)
  Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d3.id, player_id: p1.id})
  Repo.insert!(%Copi.Cornucopia.Vote{dealt_card_id: d4.id, player_id: p2.id})

  game = Cornucopia.get_game!(game.id) |> Repo.preload(players: [dealt_cards: [:card, :votes]])

  assert Cornucopia.highest_scoring_card_in_round(game, 1).id == d1.id
  # Fails on master: returns `nil` because Round 2's "Cryptography" cards are filtered out by Round 1's "Authentication" lead suit
  assert Cornucopia.highest_scoring_card_in_round(game, 2).id == d3.id
end
Expected Behavior

special_lead_cards/1 should be applied per round after grouping by played_in_round:

def highest_scoring_cards_in_game(game) do
  Enum.reduce(game.players, [], fn player, cards -> (player.dealt_cards |> played_cards) ++ cards end)
    |> lead_suit_cards
    |> scoring_cards(Enum.count(game.players))
    |> Enum.group_by(fn card -> card.played_in_round end)
    |> Enum.flat_map(fn {_, cards} -> cards |> special_lead_cards() |> highest_card() end)
end

Hi @sydseter, could you please assign this issue to me? I'd be happy to open a PR with the fix and multi-round regression tests.

主要言語
Python
スター
158
フォーク
110
平均マージ
1日 11時間
マージ済み PR(30日)
115

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

OWASP/cornucopia のほかの issue

OWASP/cornucopia の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。