From 85fd7f5420df30aba952751cde37751b69b81730 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 00:45:39 -0400 Subject: [PATCH 01/14] fix(humanitycards): use speak_l for scores and announce judge personally Replace raw user.speak() with localized speak_l("hc-score-line") in both _action_view_scores and _action_check_scores. Wire up the existing but unused hc-you-are-judge FTL key so each judge hears a personal announcement alongside the table broadcast. Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index d82a36ae..40c0785c 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -801,8 +801,8 @@ def _action_view_scores(self, player: Player, action_id: str) -> None: key=lambda p: p.score, # type: ignore reverse=True, ) - parts = [f"{p.name}: {p.score}" for p in sorted_players] # type: ignore - user.speak(". ".join(parts) + ".") + for p in sorted_players: + user.speak_l("hc-score-line", player=p.name, score=p.score) # type: ignore # ========================================================================== # Whose judge / whose turn overrides @@ -1220,8 +1220,8 @@ def _action_check_scores(self, player: Player, action_id: str) -> None: key=lambda p: p.score, # type: ignore reverse=True, ) - parts = [f"{p.name}: {p.score}" for p in sorted_players] # type: ignore - user.speak(". ".join(parts) + ".") + for p in sorted_players: + user.speak_l("hc-score-line", player=p.name, score=p.score) # type: ignore def _action_check_scores_detailed(self, player: Player, action_id: str) -> None: user = self.get_user(player) @@ -1323,6 +1323,10 @@ def _start_round(self) -> None: else: others = ", ".join(j.name for j in judges[1:]) self.broadcast_l("hc-judge-is", player=judges[0].name, count=len(judges), others=others) + for judge in judges: + user = self.get_user(judge) + if user: + user.speak_l("hc-you-are-judge") # Announce black card black_text = self._speech_friendly_black(self.current_black_card["text"]) From 7a1c7ed65d1bf6ce538890a6feb9e225ed1137e4 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 00:49:40 -0400 Subject: [PATCH 02/14] test(humanitycards): add 52 tests covering full game flow Covers: metadata, startup, judge selection (rotating/random/winner), deck reshuffle, card toggling, submission validation, judging phase, win condition, score display (speak_l fix), judge personal announcement (hc-you-are-judge fix), round transitions, and bot game completion. Co-Authored-By: Claude Sonnet 4.6 --- server/tests/test_humanitycards.py | 637 +++++++++++++++++++++++++++++ 1 file changed, 637 insertions(+) create mode 100644 server/tests/test_humanitycards.py diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py new file mode 100644 index 00000000..89329bca --- /dev/null +++ b/server/tests/test_humanitycards.py @@ -0,0 +1,637 @@ +"""Tests for Humanity Cards (Cards Against Humanity) game.""" + +import pytest + +from server.core.users.bot import Bot +from server.core.users.test_user import MockUser +from server.games.humanitycards.game import HumanityCardsGame, HumanityCardsOptions + + +# ========================================================================== +# Helpers +# ========================================================================== + + +def _make_white(count: int, start: int = 0) -> list[dict]: + return [{"text": f"White card {i}", "pack": "Test", "id": i} for i in range(start, start + count)] + + +def _make_black(text: str = "Why is _ so funny?", pick: int = 1) -> dict: + return {"text": text, "pick": pick, "pack": "Test"} + + +def _inject_decks(game: HumanityCardsGame, white_count: int = 200, black_count: int = 50) -> None: + """Replace decks with deterministic test cards.""" + game.white_deck = _make_white(white_count) + game.black_deck = [_make_black(f"Question {i} _") for i in range(black_count)] + game.white_discard = [] + game.black_discard = [] + + +def _setup_game( + num_players: int = 3, + options: HumanityCardsOptions | None = None, + use_bots: bool = False, +) -> tuple[HumanityCardsGame, list[MockUser]]: + opts = options or HumanityCardsOptions() + game = HumanityCardsGame(options=opts) + game._build_decks = lambda: _inject_decks(game) # type: ignore[method-assign] + users = [] + for i in range(num_players): + name = f"Player{i}" + if use_bots: + user: MockUser | Bot = Bot(name) + else: + user = MockUser(name) + game.add_player(name, user) + if not use_bots: + users.append(user) # type: ignore[arg-type] + game.on_start() + return game, users + + +def _get_player(game: HumanityCardsGame, index: int): + return game.get_active_players()[index] + + +# ========================================================================== +# Metadata & options +# ========================================================================== + + +def test_game_metadata(): + game = HumanityCardsGame() + assert game.get_name() == "Cards Against Humanity" + assert game.get_type() == "humanitycards" + assert game.get_min_players() == 3 + assert game.get_max_players() >= 6 + + +def test_options_defaults(): + opts = HumanityCardsOptions() + assert opts.winning_score == 7 + assert opts.hand_size == 10 + assert opts.czar_selection == "Rotating" + assert opts.num_judges == 1 + + +# ========================================================================== +# Game startup +# ========================================================================== + + +def test_game_starts_in_submitting_phase(): + game, _ = _setup_game() + assert game.phase == "submitting" + assert game.status == "playing" + assert game.round == 1 + + +def test_players_dealt_hands_on_start(): + game, _ = _setup_game(num_players=3) + for p in game.get_active_players(): + assert len(p.hand) == game.options.hand_size # type: ignore[union-attr] + + +def test_player_scores_zero_on_start(): + game, _ = _setup_game() + for p in game.get_active_players(): + assert p.score == 0 # type: ignore[union-attr] + + +def test_black_card_dealt_on_start(): + game, _ = _setup_game() + assert game.current_black_card is not None + assert "text" in game.current_black_card + assert "pick" in game.current_black_card + + +# ========================================================================== +# Judge selection +# ========================================================================== + + +def test_one_judge_on_start(): + game, _ = _setup_game() + judges = game._get_judges() + assert len(judges) == 1 + + +def test_rotating_judge_advances_each_round(): + game, _ = _setup_game(num_players=4) + first_judge_id = game._get_judges()[0].id + # Simulate completing a round by triggering next round + game._start_round() + second_judge_id = game._get_judges()[0].id + assert first_judge_id != second_judge_id + + +def test_judge_count_never_exceeds_active_minus_one(): + game, _ = _setup_game(num_players=3, options=HumanityCardsOptions(num_judges=3)) + # 3 players, max judges = active - 1 = 2 + judges = game._get_judges() + assert len(judges) <= len(game.get_active_players()) - 1 + + +def test_random_judge_selection_picks_valid_player(): + game, _ = _setup_game(options=HumanityCardsOptions(czar_selection="Random")) + judges = game._get_judges() + active_ids = {p.id for p in game.get_active_players()} + for j in judges: + assert j.id in active_ids + + +def test_winner_judge_selection_uses_last_winner(): + game, _ = _setup_game( + num_players=4, options=HumanityCardsOptions(czar_selection="Most Recent Winner") + ) + active = game.get_active_players() + game.last_winner_index = 2 + game._start_round() + judges = game._get_judges() + assert judges[0].id == active[2].id + + +def test_judge_personal_announcement_spoken(): + game, users = _setup_game(num_players=3) + judge = game._get_judges()[0] + judge_user = next(u for u in users if u.username == judge.name) + spoken = judge_user.get_spoken_messages() + assert any("Card Czar" in m for m in spoken) + + +# ========================================================================== +# Utility methods +# ========================================================================== + + +def test_fill_in_blanks_single(): + game, _ = _setup_game() + result = game._fill_in_blanks("I love _.", ["cats"]) + assert result == "I love cats." + + +def test_fill_in_blanks_multiple(): + game, _ = _setup_game() + result = game._fill_in_blanks("_ meets _.", ["Alice", "Bob"]) + assert result == "Alice meets Bob." + + +def test_fill_in_blanks_no_blank_appends(): + game, _ = _setup_game() + result = game._fill_in_blanks("Why?", ["Because reasons"]) + assert result == "Why? Because reasons" + + +def test_speech_friendly_black_replaces_underscore(): + game, _ = _setup_game() + assert game._speech_friendly_black("I love _.") == "I love blank." + + +def test_speech_friendly_black_multiple(): + game, _ = _setup_game() + assert game._speech_friendly_black("_ and _.") == "blank and blank." + + +# ========================================================================== +# Deck reshuffle +# ========================================================================== + + +def test_white_deck_reshuffles_from_discard(): + game, _ = _setup_game() + # Drain white deck + game.white_deck = [] + game.white_discard = _make_white(5, start=100) + drawn = game._draw_white(3) + assert len(drawn) == 3 + # Remaining discard minus drawn cards + assert len(game.white_deck) + len(drawn) == 5 + + +def test_white_deck_reshuffle_broadcasts(): + game, users = _setup_game() + game.white_deck = [] + game.white_discard = _make_white(5, start=100) + for u in users: + u.clear_messages() + game._draw_white(1) + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert any("reshuffled" in m.lower() for m in all_spoken) + + +def test_black_deck_reshuffles_from_discard(): + game, _ = _setup_game() + game.black_deck = [] + game.black_discard = [_make_black("Test _ card") for _ in range(3)] + card = game._draw_black() + assert card is not None + + +def test_draw_white_returns_empty_list_when_no_cards(): + game, _ = _setup_game() + game.white_deck = [] + game.white_discard = [] + drawn = game._draw_white(5) + assert drawn == [] + + +# ========================================================================== +# Card toggling +# ========================================================================== + + +def test_toggle_card_selects_card(): + game, users = _setup_game() + non_judge = game._get_non_judges()[0] + assert len(non_judge.selected_indices) == 0 + game.execute_action(non_judge, "toggle_card_0") + assert 0 in non_judge.selected_indices + + +def test_toggle_card_deselects_card(): + game, _ = _setup_game() + non_judge = game._get_non_judges()[0] + game.execute_action(non_judge, "toggle_card_0") + assert 0 in non_judge.selected_indices + game.execute_action(non_judge, "toggle_card_0") + assert 0 not in non_judge.selected_indices + + +def test_judge_cannot_toggle_cards(): + game, _ = _setup_game() + judge = game._get_judges()[0] + game.execute_action(judge, "toggle_card_0") + assert 0 not in judge.selected_indices # type: ignore[union-attr] + + +def test_card_toggle_plays_select_sound(): + game, users = _setup_game() + non_judge = game._get_non_judges()[0] + non_judge_user = next(u for u in users if u.username == non_judge.name) + non_judge_user.clear_messages() + game.execute_action(non_judge, "toggle_card_0") + sounds = non_judge_user.get_sounds_played() + assert any("cardselect" in s for s in sounds) + + +def test_card_toggle_plays_unselect_sound(): + game, users = _setup_game() + non_judge = game._get_non_judges()[0] + non_judge_user = next(u for u in users if u.username == non_judge.name) + game.execute_action(non_judge, "toggle_card_0") + non_judge_user.clear_messages() + game.execute_action(non_judge, "toggle_card_0") + sounds = non_judge_user.get_sounds_played() + assert any("cardunselect" in s for s in sounds) + + +# ========================================================================== +# Submission +# ========================================================================== + + +def test_submit_cards_removes_from_hand(): + game, _ = _setup_game() + non_judge = game._get_non_judges()[0] + hand_size_before = len(non_judge.hand) + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + assert non_judge.submitted_cards is not None + assert len(non_judge.hand) == hand_size_before - 1 + + +def test_submit_cards_records_submission(): + game, _ = _setup_game() + non_judge = game._get_non_judges()[0] + expected_text = non_judge.hand[0]["text"] + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + assert non_judge.submitted_cards == [expected_text] + + +def test_submit_wrong_count_rejected(): + game, users = _setup_game() + # Force a pick-2 black card + game.current_black_card = _make_black("_ loves _ forever.", pick=2) + non_judge = game._get_non_judges()[0] + non_judge_user = next(u for u in users if u.username == non_judge.name) + # Select only 1 card, need 2 + game.execute_action(non_judge, "toggle_card_0") + non_judge_user.clear_messages() + game.execute_action(non_judge, "submit_cards") + assert non_judge.submitted_cards is None + spoken = non_judge_user.get_spoken_messages() + assert any("2" in m for m in spoken) + + +def test_submit_already_submitted_rejected(): + game, users = _setup_game() + non_judge = game._get_non_judges()[0] + non_judge_user = next(u for u in users if u.username == non_judge.name) + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + submission_after_first = list(non_judge.submitted_cards) # type: ignore[arg-type] + non_judge_user.clear_messages() + # Try to submit again (no selected cards, already submitted) + game.execute_action(non_judge, "submit_cards") + assert non_judge.submitted_cards == submission_after_first + + +def test_judge_cannot_submit(): + game, _ = _setup_game() + judge = game._get_judges()[0] + game.execute_action(judge, "toggle_card_0") + game.execute_action(judge, "submit_cards") + assert judge.submitted_cards is None # type: ignore[union-attr] + + +def test_all_submit_triggers_judging_phase(): + game, _ = _setup_game(num_players=3) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + assert game.phase == "judging" + + +def test_submission_progress_broadcast(): + game, users = _setup_game(num_players=3) + for u in users: + u.clear_messages() + non_judge = game._get_non_judges()[0] + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert any("submitted" in m.lower() or "of" in m for m in all_spoken) + + +def test_pick_two_black_card_requires_two_submissions(): + game, _ = _setup_game() + game.current_black_card = _make_black("_ with _ always.", pick=2) + non_judge = game._get_non_judges()[0] + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "toggle_card_1") + game.execute_action(non_judge, "submit_cards") + assert non_judge.submitted_cards is not None + assert len(non_judge.submitted_cards) == 2 + + +# ========================================================================== +# Judging +# ========================================================================== + + +def _get_to_judging(num_players: int = 3): + game, users = _setup_game(num_players=num_players) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + assert game.phase == "judging" + return game, users + + +def test_judge_pick_awards_point(): + game, _ = _get_to_judging() + judge = game._get_judges()[0] + winner_before = {p.id: p.score for p in game.get_active_players()} # type: ignore[union-attr] + game.execute_action(judge, "judge_pick_0") + winner_id = game.submissions[game.submission_order[0]]["player_id"] + winner = game.get_player_by_id(winner_id) + assert winner.score == winner_before[winner_id] + 1 # type: ignore[union-attr] + + +def test_judge_pick_transitions_to_round_end(): + game, _ = _get_to_judging() + judge = game._get_judges()[0] + game.execute_action(judge, "judge_pick_0") + assert game.phase == "round_end" + + +def test_winner_announcement_broadcast(): + game, users = _get_to_judging() + for u in users: + u.clear_messages() + judge = game._get_judges()[0] + game.execute_action(judge, "judge_pick_0") + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert any("wins" in m.lower() for m in all_spoken) + + +def test_non_judge_cannot_pick(): + game, _ = _get_to_judging() + non_judge = game._get_non_judges()[0] + # Non-judges have no valid submissions to pick at this point, action is hidden + submissions_before = list(game.submissions) + game.execute_action(non_judge, "judge_pick_0") + # State unchanged + assert game.submissions == submissions_before + assert game.phase == "judging" + + +def test_submissions_shuffled_before_judging(): + # Run many times; at least some ordering should differ from insertion order + game, _ = _setup_game(num_players=4) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + # submission_order exists and covers all submissions + assert len(game.submission_order) == len(game.submissions) + assert sorted(game.submission_order) == list(range(len(game.submissions))) + + +# ========================================================================== +# Win condition +# ========================================================================== + + +def test_game_ends_when_winning_score_reached(): + game, _ = _setup_game(options=HumanityCardsOptions(winning_score=1)) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + judge = game._get_judges()[0] + game.execute_action(judge, "judge_pick_0") + assert game.status == "finished" + + +def test_round_continues_when_score_below_winning(): + game, _ = _setup_game(options=HumanityCardsOptions(winning_score=5)) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + judge = game._get_judges()[0] + game.execute_action(judge, "judge_pick_0") + assert game.status == "playing" + assert game.phase == "round_end" + + +def test_game_winner_broadcast(): + game, users = _setup_game(options=HumanityCardsOptions(winning_score=1)) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + for u in users: + u.clear_messages() + judge = game._get_judges()[0] + game.execute_action(judge, "judge_pick_0") + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert any("wins" in m.lower() for m in all_spoken) + + +# ========================================================================== +# Score display (fix 3: speak_l not raw speak) +# ========================================================================== + + +def test_view_scores_speaks_all_players(): + game, users = _setup_game(num_players=3) + player0 = _get_player(game, 0) + player0.score = 3 # type: ignore[union-attr] + user0 = next(u for u in users if u.username == player0.name) + user0.clear_messages() + game.execute_action(player0, "view_scores") + spoken = user0.get_spoken_messages() + assert any(player0.name in m for m in spoken) + + +def test_view_scores_includes_score_values(): + game, users = _setup_game(num_players=3) + player0 = _get_player(game, 0) + player0.score = 5 # type: ignore[union-attr] + user0 = next(u for u in users if u.username == player0.name) + user0.clear_messages() + game.execute_action(player0, "view_scores") + spoken = user0.get_spoken_messages() + # Score "5" or "5 points" should appear + assert any("5" in m for m in spoken) + + +def test_view_scores_ordered_descending(): + game, users = _setup_game(num_players=3) + active = game.get_active_players() + active[0].score = 5 # type: ignore[union-attr] + active[1].score = 3 # type: ignore[union-attr] + active[2].score = 1 # type: ignore[union-attr] + user0 = next(u for u in users if u.username == active[0].name) + user0.clear_messages() + game.execute_action(active[0], "view_scores") + spoken = user0.get_spoken_messages() + # First spoken message should mention the highest scorer + assert active[0].name in spoken[0] + + +def test_check_scores_speaks_all_players(): + game, users = _setup_game(num_players=3) + player0 = _get_player(game, 0) + user0 = next(u for u in users if u.username == player0.name) + user0.clear_messages() + game.execute_action(player0, "check_scores") + spoken = user0.get_spoken_messages() + assert len(spoken) == 3 # One line per player + + +# ========================================================================== +# Judge personal announcement (fix 4: hc-you-are-judge) +# ========================================================================== + + +def test_judge_hears_you_are_judge_message(): + game, users = _setup_game(num_players=3) + judge = game._get_judges()[0] + judge_user = next(u for u in users if u.username == judge.name) + spoken = judge_user.get_spoken_messages() + assert any("Card Czar" in m for m in spoken) + + +def test_non_judge_does_not_hear_you_are_judge(): + game, users = _setup_game(num_players=3) + judge_ids = {j.id for j in game._get_judges()} + non_judge_users = [ + u for u in users + if game.get_player_by_id( + next((p.id for p in game.get_active_players() if p.name == u.username), "") + ) is not None + and next( + (p for p in game.get_active_players() if p.name == u.username), None + ) is not None + and next(p for p in game.get_active_players() if p.name == u.username).id not in judge_ids + ] + for u in non_judge_users: + spoken = u.get_spoken_messages() + # Non-judges hear "X is the Card Czar" (broadcast) but NOT "You are the Card Czar" + assert not any(m.startswith("You are the Card Czar") for m in spoken) + + +def test_judge_announcement_fires_each_new_round(): + game, users = _setup_game(num_players=3) + # Complete submissions to advance round + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + first_judge = game._get_judges()[0] + old_judge_user = next(u for u in users if u.username == first_judge.name) + old_judge_user.clear_messages() + # Pick winner → round_end → next round + game.execute_action(first_judge, "judge_pick_0") + # Advance tick countdown to trigger next round + game.round_end_ticks = 1 + game.on_tick() + # New judge should have received announcement + new_judge = game._get_judges()[0] + new_judge_user = next(u for u in users if u.username == new_judge.name) + spoken = new_judge_user.get_spoken_messages() + assert any("Card Czar" in m for m in spoken) + + +# ========================================================================== +# Round transition via ticks +# ========================================================================== + + +def test_round_end_ticks_advance_to_next_round(): + game, _ = _setup_game() + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + game.execute_action(game._get_judges()[0], "judge_pick_0") + assert game.phase == "round_end" + game.round_end_ticks = 1 + game.on_tick() + assert game.phase == "submitting" + assert game.round == 2 + + +# ========================================================================== +# Full bot game +# ========================================================================== + + +def test_bot_game_completes(): + opts = HumanityCardsOptions(winning_score=3) + game = HumanityCardsGame(options=opts) + game._build_decks = lambda: _inject_decks(game, white_count=500, black_count=100) # type: ignore[method-assign] + for i in range(4): + game.add_player(f"Bot{i}", Bot(f"Bot{i}")) + game.on_start() + + for _ in range(100_000): + if game.status == "finished": + break + game.on_tick() + + assert game.status == "finished" + + +def test_bot_game_all_players_score_tracked(): + opts = HumanityCardsOptions(winning_score=2) + game = HumanityCardsGame(options=opts) + game._build_decks = lambda: _inject_decks(game, white_count=500, black_count=100) # type: ignore[method-assign] + for i in range(3): + game.add_player(f"Bot{i}", Bot(f"Bot{i}")) + game.on_start() + for _ in range(100_000): + if game.status == "finished": + break + game.on_tick() + total_score = sum(p.score for p in game.get_active_players()) # type: ignore[union-attr] + assert total_score >= opts.winning_score From f23d7e84e42db6566c16f48986eb12bd7f75a652 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 01:04:09 -0400 Subject: [PATCH 03/14] refactor(humanitycards): remove duplicate view_scores action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleted custom view_scores action, its keybind, and handler — inherited check_scores from the base game does the same thing. Renamed the shared enabled-check to _is_playing_enabled. Updated tests to use check_scores. Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 49 ++---------------------------- server/tests/test_humanitycards.py | 12 ++++---- 2 files changed, 9 insertions(+), 52 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index 40c0785c..e5c05cf5 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -522,25 +522,13 @@ def create_turn_action_set(self, player: HumanityCardsPlayer) -> ActionSet: ) ) - # View scores (always visible at bottom) - action_set.add( - Action( - id="view_scores", - label=Localization.get(locale, "hc-view-scores"), - handler="_action_view_scores", - is_enabled="_is_view_scores_enabled", - is_hidden="_is_view_scores_hidden", - show_in_actions_menu=True, - ) - ) - # Whose judge (keybind-only, J key) action_set.add( Action( id="whose_judge", label=Localization.get(locale, "hc-whose-judge"), handler="_action_whose_judge", - is_enabled="_is_view_scores_enabled", + is_enabled="_is_playing_enabled", is_hidden="_is_whose_judge_hidden", ) ) @@ -586,15 +574,6 @@ def setup_keybinds(self) -> None: state=KeybindState.ACTIVE, ) - # S to view scores - self.define_keybind( - "s", - "View scores", - ["view_scores"], - state=KeybindState.ACTIVE, - include_spectators=True, - ) - # J to announce judges self.define_keybind( "j", @@ -778,36 +757,14 @@ def _get_submission_options(self, player: Player) -> list[str]: return options # ========================================================================== - # View scores callbacks + # Whose judge / whose turn overrides # ========================================================================== - def _is_view_scores_enabled(self, player: Player) -> str | None: + def _is_playing_enabled(self, player: Player) -> str | None: if self.status != "playing": return "action-not-playing" return None - def _is_view_scores_hidden(self, player: Player) -> Visibility: - if self.status != "playing": - return Visibility.HIDDEN - return Visibility.VISIBLE - - def _action_view_scores(self, player: Player, action_id: str) -> None: - """View the current scores.""" - user = self.get_user(player) - if not user: - return - sorted_players = sorted( - self.get_active_players(), - key=lambda p: p.score, # type: ignore - reverse=True, - ) - for p in sorted_players: - user.speak_l("hc-score-line", player=p.name, score=p.score) # type: ignore - - # ========================================================================== - # Whose judge / whose turn overrides - # ========================================================================== - def _is_whose_judge_hidden(self, player: Player) -> Visibility: # Keybind-only — always hidden from menu return Visibility.HIDDEN diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py index 89329bca..26f66877 100644 --- a/server/tests/test_humanitycards.py +++ b/server/tests/test_humanitycards.py @@ -483,30 +483,30 @@ def test_game_winner_broadcast(): # ========================================================================== -def test_view_scores_speaks_all_players(): +def test_check_scores_speaks_all_players_v2(): game, users = _setup_game(num_players=3) player0 = _get_player(game, 0) player0.score = 3 # type: ignore[union-attr] user0 = next(u for u in users if u.username == player0.name) user0.clear_messages() - game.execute_action(player0, "view_scores") + game.execute_action(player0, "check_scores") spoken = user0.get_spoken_messages() assert any(player0.name in m for m in spoken) -def test_view_scores_includes_score_values(): +def test_check_scores_includes_score_values(): game, users = _setup_game(num_players=3) player0 = _get_player(game, 0) player0.score = 5 # type: ignore[union-attr] user0 = next(u for u in users if u.username == player0.name) user0.clear_messages() - game.execute_action(player0, "view_scores") + game.execute_action(player0, "check_scores") spoken = user0.get_spoken_messages() # Score "5" or "5 points" should appear assert any("5" in m for m in spoken) -def test_view_scores_ordered_descending(): +def test_check_scores_ordered_descending(): game, users = _setup_game(num_players=3) active = game.get_active_players() active[0].score = 5 # type: ignore[union-attr] @@ -514,7 +514,7 @@ def test_view_scores_ordered_descending(): active[2].score = 1 # type: ignore[union-attr] user0 = next(u for u in users if u.username == active[0].name) user0.clear_messages() - game.execute_action(active[0], "view_scores") + game.execute_action(active[0], "check_scores") spoken = user0.get_spoken_messages() # First spoken message should mention the highest scorer assert active[0].name in spoken[0] From e12bc030a0ca790dca76c7d28d25c2c495e9371b Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 01:13:10 -0400 Subject: [PATCH 04/14] fix(humanitycards): fix check_scores_detailed localization, drop orphaned FTL keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _action_check_scores_detailed was using a hardcoded English f-string; replaced with Localization.get("hc-score-line"). Restored the enabled callbacks (_is_check_scores_enabled, _is_check_scores_detailed_enabled) to bypass the base class team_manager check — CAH uses player.score directly. Removed orphaned FTL keys: hc-view-scores, hc-no-scores, hc-waiting-for-submissions. Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 18 ++++-------------- server/locales/en/humanitycards.ftl | 4 ---- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index e5c05cf5..980b43ac 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -1155,7 +1155,7 @@ def _action_view_submission(self, player: Player, action_id: str) -> None: user.speak_l("hc-select-cards-first") # ========================================================================== - # Score overrides + # Score overrides (CAH uses player.score, not team_manager) # ========================================================================== def _is_check_scores_enabled(self, player: Player) -> str | None: @@ -1172,26 +1172,16 @@ def _action_check_scores(self, player: Player, action_id: str) -> None: user = self.get_user(player) if not user: return - sorted_players = sorted( - self.get_active_players(), - key=lambda p: p.score, # type: ignore - reverse=True, - ) - for p in sorted_players: + for p in sorted(self.get_active_players(), key=lambda p: p.score, reverse=True): # type: ignore user.speak_l("hc-score-line", player=p.name, score=p.score) # type: ignore def _action_check_scores_detailed(self, player: Player, action_id: str) -> None: user = self.get_user(player) if not user: return - sorted_players = sorted( - self.get_active_players(), - key=lambda p: p.score, # type: ignore - reverse=True, - ) lines = [ - f"{p.name}: {p.score} points" # type: ignore - for p in sorted_players + Localization.get(user.locale, "hc-score-line", player=p.name, score=p.score) # type: ignore + for p in sorted(self.get_active_players(), key=lambda p: p.score, reverse=True) # type: ignore ] self.status_box(player, lines) diff --git a/server/locales/en/humanitycards.ftl b/server/locales/en/humanitycards.ftl index aa8aa537..e4fab551 100644 --- a/server/locales/en/humanitycards.ftl +++ b/server/locales/en/humanitycards.ftl @@ -56,7 +56,6 @@ hc-submit-cards = Submit ({ $selected } of { $required } selected) hc-submitted = You submitted your cards. hc-player-submitted = { $player } submitted. hc-submission-progress = { $submitted } of { $total } players submitted. -hc-waiting-for-submissions = Waiting for submissions... hc-already-submitted = You already submitted your cards. hc-wrong-card-count = You need to select exactly { $count } { $count -> [one] card @@ -99,9 +98,6 @@ hc-not-enough-cards = Not enough cards. Try enabling more packs. hc-view-hand = View hand # Scores -hc-view-scores = View scores -hc-no-scores = No scores yet. - # Whose turn / whose judge hc-whose-judge = Who is judging hc-waiting-for = Waiting for { $names } to submit. From 47518d61de5cd16368b6466771bed0276ec7fce4 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 01:32:43 -0400 Subject: [PATCH 05/14] fix(humanitycards): fix localization error, submission noise, multi-judge voting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove parametrized tuple from _is_submit_enabled — framework doesn't support it; action handler already validates and speaks hc-wrong-card-count - Remove hc-submission-progress broadcast — sound-only feedback sufficient - Rewrite _judge_pick for multi-judge: record each judge's vote in judge_picks, wait for all judges, then tally votes (most wins, tiebreak by earliest submission_order position). Judges can no longer re-vote. - Add hc-judge-voted FTL key for intermediate broadcast when waiting - Add 8 new tests covering multi-judge flow and fixed bugs (60 total) Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 97 ++++++++++++------------- server/locales/en/humanitycards.ftl | 1 + server/tests/test_humanitycards.py | 109 +++++++++++++++++++++++++++- 3 files changed, 156 insertions(+), 51 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index 980b43ac..d46ad5fd 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -235,6 +235,7 @@ class HumanityCardsGame(Game): last_winner_index: int = -1 # For "Most Recent Winner" czar selection submissions: list[dict] = field(default_factory=list) # [{"player_id": str, "cards": [str]}] submission_order: list[int] = field(default_factory=list) # Shuffled indices into submissions + judge_picks: dict[str, str] = field(default_factory=dict) # judge_id → picked player_id round_end_ticks: int = 0 # Countdown ticks before next round starts @classmethod @@ -638,7 +639,7 @@ def _get_toggle_card_sound(self, player: Player, action_id: str) -> str | None: return "game_humanitycards/cardselect.ogg" return None - def _is_submit_enabled(self, player: Player) -> str | tuple[str, dict] | None: + def _is_submit_enabled(self, player: Player) -> str | None: if self.status != "playing": return "action-not-playing" if player.is_spectator: @@ -650,9 +651,6 @@ def _is_submit_enabled(self, player: Player) -> str | tuple[str, dict] | None: return "hc-already-submitted" if self.phase != "submitting": return "action-not-playing" - required = self.current_black_card["pick"] if self.current_black_card else 1 - if len(hcp.selected_indices) != required: - return ("hc-wrong-card-count", {"count": required}) return None def _is_submit_hidden(self, player: Player) -> Visibility: @@ -691,6 +689,8 @@ def _is_judge_pick_enabled(self, player: Player, action_id: str) -> str | None: return "action-spectator" if self.phase != "judging": return "action-not-playing" + if hcp.id in self.judge_picks: + return "action-already-done" idx = int(action_id.removeprefix("judge_pick_")) if idx >= len(self.submission_order): return "action-not-playing" @@ -1028,19 +1028,12 @@ def _action_submit_cards(self, player: Player, action_id: str) -> None: if user: user.speak_l("hc-submitted") - # Broadcast progress - non_judges = self._get_non_judges() - submitted_count = sum(1 for p in non_judges if p.submitted_cards is not None) - total = len(non_judges) - self.broadcast_l( - "hc-submission-progress", - submitted=submitted_count, - total=total, - ) - self.rebuild_all_menus() # Check if all have submitted + non_judges = self._get_non_judges() + submitted_count = sum(1 for p in non_judges if p.submitted_cards is not None) + total = len(non_judges) if submitted_count >= total: self._start_judging() @@ -1051,49 +1044,64 @@ def _judge_pick(self, player: Player, pick_index: int) -> None: hcp: HumanityCardsPlayer = player # type: ignore if not self._is_judge(hcp): return + if hcp.id in self.judge_picks: + return if pick_index >= len(self.submission_order): return actual_idx = self.submission_order[pick_index] if actual_idx >= len(self.submissions): return - winning_sub = self.submissions[actual_idx] - winner = self.get_player_by_id(winning_sub["player_id"]) - if not winner: + picked_player_id = self.submissions[actual_idx]["player_id"] + self.judge_picks[hcp.id] = picked_player_id + + judges = self._get_judges() + judges_still_pending = [j for j in judges if j.id not in self.judge_picks] + + if judges_still_pending: + self.play_sound(f"game_humanitycards/judgechoice{random.randint(1, 3)}.ogg") # nosec B311 + self.broadcast_l("hc-judge-voted", player=hcp.name) + self.rebuild_all_menus() return + # All judges have voted — tally + vote_counts: dict[str, int] = {} + for voted_id in self.judge_picks.values(): + vote_counts[voted_id] = vote_counts.get(voted_id, 0) + 1 + + max_votes = max(vote_counts.values()) + candidates = [pid for pid, v in vote_counts.items() if v == max_votes] + + if len(candidates) == 1: + winning_player_id = candidates[0] + else: + # Tiebreak: earliest position in shuffled submission_order + def _order_pos(pid: str) -> int: + for pos, sub_idx in enumerate(self.submission_order): + if sub_idx < len(self.submissions) and self.submissions[sub_idx]["player_id"] == pid: + return pos + return len(self.submission_order) + winning_player_id = min(candidates, key=_order_pos) + + winning_sub = next(s for s in self.submissions if s["player_id"] == winning_player_id) + winner = self.get_player_by_id(winning_player_id) + if not winner: + return hc_winner: HumanityCardsPlayer = winner # type: ignore - # Award point hc_winner.score += 1 active = self.get_active_players() self.last_winner_index = next((i for i, p in enumerate(active) if p.id == winner.id), -1) - # Announce winner winning_text = self._fill_in_blanks( self.current_black_card["text"] if self.current_black_card else "", winning_sub["cards"], ) - # Play judge choice sound - self.play_sound( - f"game_humanitycards/judgechoice{random.randint(1, 3)}.ogg" # nosec B311 - ) - - self.broadcast_l( - "hc-winner-announcement", - player=winner.name, - score=hc_winner.score, - ) - - # Announce winner's submission first - self.broadcast_l( - "hc-submission-reveal", - player=winner.name, - text=winning_text, - ) + self.play_sound(f"game_humanitycards/judgechoice{random.randint(1, 3)}.ogg") # nosec B311 + self.broadcast_l("hc-winner-announcement", player=winner.name, score=hc_winner.score) + self.broadcast_l("hc-submission-reveal", player=winner.name, text=winning_text) - # Then announce other submissions self.broadcast_l("hc-all-submissions") for sub in self.submissions: if sub["player_id"] == winner.id: @@ -1104,28 +1112,18 @@ def _judge_pick(self, player: Player, pick_index: int) -> None: self.current_black_card["text"] if self.current_black_card else "", sub["cards"], ) - self.broadcast_l( - "hc-submission-reveal", - player=sub_player.name, - text=filled, - ) + self.broadcast_l("hc-submission-reveal", player=sub_player.name, text=filled) - # Play draw card sound as players receive new cards self.play_sound(f"game_cards/draw{random.randint(1, 4)}.ogg") # nosec B311 - # Check win condition if hc_winner.score >= self.options.winning_score: self._end_game(hc_winner) else: - # Transition to round_end with delay before next round self.phase = "round_end" - self.round_end_ticks = 100 # ~5 seconds at 20 ticks/sec - - # Discard current black card + self.round_end_ticks = 100 if self.current_black_card: self.black_discard.append(self.current_black_card) self.current_black_card = None - self.rebuild_all_menus() def _action_view_black_card(self, player: Player, action_id: str) -> None: @@ -1313,6 +1311,7 @@ def _start_judging(self) -> None: self.submission_order = list(range(len(self.submissions))) random.shuffle(self.submission_order) # nosec B311 + self.judge_picks = {} self.play_sound("game_humanitycards/judging.ogg") self.broadcast_l("hc-judging-start") diff --git a/server/locales/en/humanitycards.ftl b/server/locales/en/humanitycards.ftl index e4fab551..ae862ffa 100644 --- a/server/locales/en/humanitycards.ftl +++ b/server/locales/en/humanitycards.ftl @@ -64,6 +64,7 @@ hc-wrong-card-count = You need to select exactly { $count } { $count -> # Judging phase hc-judging-start = All cards are in! Time to judge. +hc-judge-voted = { $player } has made their choice. hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py index 26f66877..e9755f5a 100644 --- a/server/tests/test_humanitycards.py +++ b/server/tests/test_humanitycards.py @@ -354,7 +354,8 @@ def test_all_submit_triggers_judging_phase(): assert game.phase == "judging" -def test_submission_progress_broadcast(): +def test_submission_no_progress_broadcast(): + # Progress broadcast was removed — sound only, no speech game, users = _setup_game(num_players=3) for u in users: u.clear_messages() @@ -362,7 +363,8 @@ def test_submission_progress_broadcast(): game.execute_action(non_judge, "toggle_card_0") game.execute_action(non_judge, "submit_cards") all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert any("submitted" in m.lower() or "of" in m for m in all_spoken) + # Only the submitter hears "You submitted your cards." — no "N of M" broadcast + assert not any("of" in m and "player" in m.lower() for m in all_spoken) def test_pick_two_black_card_requires_two_submissions(): @@ -584,6 +586,109 @@ def test_judge_announcement_fires_each_new_round(): # ========================================================================== +# ========================================================================== +# Multi-judge voting +# ========================================================================== + + +def _setup_multi_judge(num_judges: int = 2, num_players: int = 4): + game, users = _setup_game( + num_players=num_players, + options=HumanityCardsOptions(num_judges=num_judges), + ) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + assert game.phase == "judging" + return game, users + + +def test_multi_judge_waits_for_all_judges(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4) + judges = game._get_judges() + assert len(judges) == 2 + # First judge picks — should NOT end round yet + game.execute_action(judges[0], "judge_pick_0") + assert game.phase == "judging" + assert len(game.judge_picks) == 1 + + +def test_multi_judge_resolves_after_all_pick(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4) + judges = game._get_judges() + game.execute_action(judges[0], "judge_pick_0") + game.execute_action(judges[1], "judge_pick_0") + assert game.phase in ("round_end", "finished") + + +def test_multi_judge_cannot_vote_twice(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4) + judges = game._get_judges() + game.execute_action(judges[0], "judge_pick_0") + first_picks = dict(game.judge_picks) + game.execute_action(judges[0], "judge_pick_1") # attempt second vote + assert game.judge_picks == first_picks + + +def test_multi_judge_majority_wins(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4) + judges = game._get_judges() + # Both judges pick submission 0 → that player wins + sub0_player_id = game.submissions[game.submission_order[0]]["player_id"] + game.execute_action(judges[0], "judge_pick_0") + game.execute_action(judges[1], "judge_pick_0") + winner = game.get_player_by_id(sub0_player_id) + assert winner.score == 1 # type: ignore[union-attr] + + +def test_multi_judge_split_vote_tiebreak_by_order(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4) + judges = game._get_judges() + assert len(game.submissions) >= 2 + # Judges split — submission_order[0] should win tiebreak + sub0_player_id = game.submissions[game.submission_order[0]]["player_id"] + game.execute_action(judges[0], "judge_pick_0") + game.execute_action(judges[1], "judge_pick_1") + winner = game.get_player_by_id(sub0_player_id) + assert winner.score == 1 # type: ignore[union-attr] + + +def test_multi_judge_voted_broadcast(): + game, users = _setup_multi_judge(num_judges=2, num_players=4) + for u in users: + u.clear_messages() + judges = game._get_judges() + game.execute_action(judges[0], "judge_pick_0") + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert any("made their choice" in m for m in all_spoken) + + +def test_single_judge_no_waiting_broadcast(): + game, users = _get_to_judging(num_players=3) + for u in users: + u.clear_messages() + judge = game._get_judges()[0] + game.execute_action(judge, "judge_pick_0") + all_spoken = [m for u in users for m in u.get_spoken_messages()] + # No "made their choice" intermediate broadcast — single judge goes straight to result + assert not any("made their choice" in m for m in all_spoken) + + +def test_wrong_card_count_speaks_error_not_raw_tuple(): + game, users = _setup_game(num_players=3) + game.current_black_card = _make_black("_ with _ always.", pick=2) + non_judge = game._get_non_judges()[0] + non_judge_user = next(u for u in users if u.username == non_judge.name) + # Select only 1 card, need 2 + game.execute_action(non_judge, "toggle_card_0") + non_judge_user.clear_messages() + game.execute_action(non_judge, "submit_cards") + spoken = non_judge_user.get_spoken_messages() + assert spoken, "should have spoken an error" + assert not any("hc-wrong-card-count" in m for m in spoken), "raw key leaked into speech" + assert any("2" in m for m in spoken) + + # Round transition via ticks # ========================================================================== From 29b1017a9818b1d09639d55ad7eaefd8bee1da0d Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 01:46:37 -0400 Subject: [PATCH 06/14] feat(humanitycards): add judging_method option (Independent/Jury/Random) Independent: each judge's vote awards 1 point to their chosen submission; all vote-getters score, announced highest-first with "gets N points for X". Jury: majority wins; tied submissions each receive 1 point. Random: method resolved randomly at judging start each round. Enforces Independent when num_judges <= 1 per spec. active_judging_method stored on game state so Random resolves consistently mid-round. Refactored _judge_pick resolution into _resolve_independent, _resolve_jury, _announce_and_score_winners, _announce_losing_submissions, _finish_round. Updated hc-winner-announcement FTL to include points and submission text. Adds 10 new tests (66 total). Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 146 ++++++++++++++++++++-------- server/locales/en/humanitycards.ftl | 14 ++- server/tests/test_humanitycards.py | 120 +++++++++++++++++------ 3 files changed, 207 insertions(+), 73 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index d46ad5fd..5153d08f 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -203,6 +203,22 @@ class HumanityCardsOptions(GameOptions): change_msg="hc-option-changed-num-judges", ) ) + judging_method: str = option_field( + MenuOption( + default="Independent", + choices=["Independent", "Jury", "Random"], + value_key="mode", + label="hc-set-judging-method", + prompt="hc-select-judging-method", + change_msg="hc-option-changed-judging-method", + choice_labels={ + "Independent": "hc-judging-method-independent", + "Jury": "hc-judging-method-jury", + "Random": "hc-judging-method-random", + }, + description="hc-desc-judging-method", + ) + ) # ========================================================================== @@ -236,6 +252,7 @@ class HumanityCardsGame(Game): submissions: list[dict] = field(default_factory=list) # [{"player_id": str, "cards": [str]}] submission_order: list[int] = field(default_factory=list) # Shuffled indices into submissions judge_picks: dict[str, str] = field(default_factory=dict) # judge_id → picked player_id + active_judging_method: str = "" # resolved method this round (handles Random) round_end_ticks: int = 0 # Countdown ticks before next round starts @classmethod @@ -1064,47 +1081,35 @@ def _judge_pick(self, player: Player, pick_index: int) -> None: self.rebuild_all_menus() return - # All judges have voted — tally - vote_counts: dict[str, int] = {} - for voted_id in self.judge_picks.values(): - vote_counts[voted_id] = vote_counts.get(voted_id, 0) + 1 - - max_votes = max(vote_counts.values()) - candidates = [pid for pid, v in vote_counts.items() if v == max_votes] - - if len(candidates) == 1: - winning_player_id = candidates[0] - else: - # Tiebreak: earliest position in shuffled submission_order - def _order_pos(pid: str) -> int: - for pos, sub_idx in enumerate(self.submission_order): - if sub_idx < len(self.submissions) and self.submissions[sub_idx]["player_id"] == pid: - return pos - return len(self.submission_order) - winning_player_id = min(candidates, key=_order_pos) - - winning_sub = next(s for s in self.submissions if s["player_id"] == winning_player_id) - winner = self.get_player_by_id(winning_player_id) - if not winner: - return - hc_winner: HumanityCardsPlayer = winner # type: ignore - - hc_winner.score += 1 - active = self.get_active_players() - self.last_winner_index = next((i for i, p in enumerate(active) if p.id == winner.id), -1) + # All judges have voted — resolve + self._resolve_judging() - winning_text = self._fill_in_blanks( - self.current_black_card["text"] if self.current_black_card else "", - winning_sub["cards"], - ) + def _submission_order_pos(self, player_id: str) -> int: + for pos, sub_idx in enumerate(self.submission_order): + if sub_idx < len(self.submissions) and self.submissions[sub_idx]["player_id"] == player_id: + return pos + return len(self.submission_order) + def _announce_and_score_winners(self, scored: list[tuple[str, int]]) -> None: + """Announce each winner in order (highest points first), award scores.""" self.play_sound(f"game_humanitycards/judgechoice{random.randint(1, 3)}.ogg") # nosec B311 - self.broadcast_l("hc-winner-announcement", player=winner.name, score=hc_winner.score) - self.broadcast_l("hc-submission-reveal", player=winner.name, text=winning_text) + for player_id, points in scored: + player = self.get_player_by_id(player_id) + if not player: + continue + hcp: HumanityCardsPlayer = player # type: ignore + hcp.score += points + sub = next((s for s in self.submissions if s["player_id"] == player_id), None) + text = self._fill_in_blanks( + self.current_black_card["text"] if self.current_black_card else "", + sub["cards"] if sub else [], + ) + self.broadcast_l("hc-winner-announcement", player=player.name, points=points, text=text, score=hcp.score) + def _announce_losing_submissions(self, winner_ids: set[str]) -> None: self.broadcast_l("hc-all-submissions") for sub in self.submissions: - if sub["player_id"] == winner.id: + if sub["player_id"] in winner_ids: continue sub_player = self.get_player_by_id(sub["player_id"]) if sub_player: @@ -1114,17 +1119,66 @@ def _order_pos(pid: str) -> int: ) self.broadcast_l("hc-submission-reveal", player=sub_player.name, text=filled) + def _finish_round(self, primary_winner_id: str) -> None: + active = self.get_active_players() + self.last_winner_index = next( + (i for i, p in enumerate(active) if p.id == primary_winner_id), -1 + ) self.play_sound(f"game_cards/draw{random.randint(1, 4)}.ogg") # nosec B311 + # Primary winner checked first; with independent multiple scorers, pick highest + game_winner = self.get_player_by_id(primary_winner_id) + if not game_winner or game_winner.score < self.options.winning_score: # type: ignore + game_winner = next( + (p for p in active if p.score >= self.options.winning_score), # type: ignore + None, + ) + if game_winner and game_winner.score >= self.options.winning_score: # type: ignore + self._end_game(game_winner) # type: ignore + return + self.phase = "round_end" + self.round_end_ticks = 100 + if self.current_black_card: + self.black_discard.append(self.current_black_card) + self.current_black_card = None + self.rebuild_all_menus() - if hc_winner.score >= self.options.winning_score: - self._end_game(hc_winner) + def _resolve_judging(self) -> None: + if self.active_judging_method == "Independent": + self._resolve_independent() else: - self.phase = "round_end" - self.round_end_ticks = 100 - if self.current_black_card: - self.black_discard.append(self.current_black_card) - self.current_black_card = None - self.rebuild_all_menus() + self._resolve_jury() + + def _resolve_independent(self) -> None: + """Each judge's vote awards 1 point. All vote-getters score.""" + vote_counts: dict[str, int] = {} + for voted_id in self.judge_picks.values(): + vote_counts[voted_id] = vote_counts.get(voted_id, 0) + 1 + if not vote_counts: + return + # Sort: most votes first, tiebreak by submission_order position + scored = sorted( + vote_counts.items(), + key=lambda x: (-x[1], self._submission_order_pos(x[0])), + ) + self._announce_and_score_winners(scored) + self._announce_losing_submissions({pid for pid, _ in scored}) + self._finish_round(scored[0][0]) + + def _resolve_jury(self) -> None: + """Majority wins. Tied winners each get 1 point.""" + vote_counts: dict[str, int] = {} + for voted_id in self.judge_picks.values(): + vote_counts[voted_id] = vote_counts.get(voted_id, 0) + 1 + if not vote_counts: + return + max_votes = max(vote_counts.values()) + tied = [pid for pid, v in vote_counts.items() if v == max_votes] + # Sort tied winners by submission_order for consistent announcement order + tied.sort(key=self._submission_order_pos) + scored = [(pid, 1) for pid in tied] + self._announce_and_score_winners(scored) + self._announce_losing_submissions(set(tied)) + self._finish_round(tied[0]) def _action_view_black_card(self, player: Player, action_id: str) -> None: """View the current black card prompt.""" @@ -1312,6 +1366,12 @@ def _start_judging(self) -> None: random.shuffle(self.submission_order) # nosec B311 self.judge_picks = {} + method = self.options.judging_method + if len(self._get_judges()) <= 1: + method = "Independent" # enforce per spec + elif method == "Random": + method = random.choice(["Independent", "Jury"]) # nosec B311 + self.active_judging_method = method self.play_sound("game_humanitycards/judging.ogg") self.broadcast_l("hc-judging-start") diff --git a/server/locales/en/humanitycards.ftl b/server/locales/en/humanitycards.ftl index ae862ffa..2becaf92 100644 --- a/server/locales/en/humanitycards.ftl +++ b/server/locales/en/humanitycards.ftl @@ -15,6 +15,14 @@ hc-set-card-packs = Card packs ({ $count } of { $total } selected) hc-desc-card-packs = Which card packs to use hc-option-changed-card-packs = Card pack selection changed. +hc-set-judging-method = Judging method: { $mode } +hc-select-judging-method = Select judging method +hc-desc-judging-method = How winning submissions are chosen. Independent: each judge picks a winner, one point per vote. Jury: majority wins; ties award all tied players one point. Random: method chosen randomly each round. +hc-option-changed-judging-method = Judging method set to { $mode }. +hc-judging-method-independent = Independent +hc-judging-method-jury = Jury +hc-judging-method-random = Random + hc-set-czar-selection = Card Czar selection: { $mode } hc-select-czar-selection = Select Card Czar selection mode hc-option-changed-czar-selection = Card Czar selection set to { $mode }. @@ -69,8 +77,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. -hc-winner-card = Winning answer: { $text } +hc-winner-announcement = { $player } gets { $points } { $points -> + [one] point + *[other] points +} for { $text }. Score: { $score }. hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> [one] point diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py index e9755f5a..1e92eba2 100644 --- a/server/tests/test_humanitycards.py +++ b/server/tests/test_humanitycards.py @@ -416,7 +416,7 @@ def test_winner_announcement_broadcast(): judge = game._get_judges()[0] game.execute_action(judge, "judge_pick_0") all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert any("wins" in m.lower() for m in all_spoken) + assert any("gets" in m.lower() and "point" in m.lower() for m in all_spoken) def test_non_judge_cannot_pick(): @@ -477,7 +477,7 @@ def test_game_winner_broadcast(): judge = game._get_judges()[0] game.execute_action(judge, "judge_pick_0") all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert any("wins" in m.lower() for m in all_spoken) + assert any("gets" in m.lower() and "point" in m.lower() for m in all_spoken) # ========================================================================== @@ -591,10 +591,10 @@ def test_judge_announcement_fires_each_new_round(): # ========================================================================== -def _setup_multi_judge(num_judges: int = 2, num_players: int = 4): +def _setup_multi_judge(num_judges: int = 2, num_players: int = 4, judging_method: str = "Independent"): game, users = _setup_game( num_players=num_players, - options=HumanityCardsOptions(num_judges=num_judges), + options=HumanityCardsOptions(num_judges=num_judges, judging_method=judging_method), ) for non_judge in game._get_non_judges(): game.execute_action(non_judge, "toggle_card_0") @@ -607,7 +607,6 @@ def test_multi_judge_waits_for_all_judges(): game, _ = _setup_multi_judge(num_judges=2, num_players=4) judges = game._get_judges() assert len(judges) == 2 - # First judge picks — should NOT end round yet game.execute_action(judges[0], "judge_pick_0") assert game.phase == "judging" assert len(game.judge_picks) == 1 @@ -626,52 +625,117 @@ def test_multi_judge_cannot_vote_twice(): judges = game._get_judges() game.execute_action(judges[0], "judge_pick_0") first_picks = dict(game.judge_picks) - game.execute_action(judges[0], "judge_pick_1") # attempt second vote + game.execute_action(judges[0], "judge_pick_1") assert game.judge_picks == first_picks -def test_multi_judge_majority_wins(): - game, _ = _setup_multi_judge(num_judges=2, num_players=4) +def test_multi_judge_voted_broadcast(): + game, users = _setup_multi_judge(num_judges=2, num_players=4) + for u in users: + u.clear_messages() + judges = game._get_judges() + game.execute_action(judges[0], "judge_pick_0") + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert any("made their choice" in m for m in all_spoken) + + +def test_single_judge_no_waiting_broadcast(): + game, users = _get_to_judging(num_players=3) + for u in users: + u.clear_messages() + judge = game._get_judges()[0] + game.execute_action(judge, "judge_pick_0") + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert not any("made their choice" in m for m in all_spoken) + + +# ========================================================================== +# Judging methods +# ========================================================================== + + +def test_independent_single_judge_enforced(): + game, _ = _setup_game(options=HumanityCardsOptions(num_judges=1, judging_method="Jury")) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + assert game.active_judging_method == "Independent" + + +def test_independent_awards_one_point_per_vote(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Independent") judges = game._get_judges() - # Both judges pick submission 0 → that player wins sub0_player_id = game.submissions[game.submission_order[0]]["player_id"] game.execute_action(judges[0], "judge_pick_0") game.execute_action(judges[1], "judge_pick_0") winner = game.get_player_by_id(sub0_player_id) - assert winner.score == 1 # type: ignore[union-attr] + assert winner.score == 2 # type: ignore[union-attr] -def test_multi_judge_split_vote_tiebreak_by_order(): - game, _ = _setup_multi_judge(num_judges=2, num_players=4) +def test_independent_split_vote_both_score(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Independent") judges = game._get_judges() assert len(game.submissions) >= 2 - # Judges split — submission_order[0] should win tiebreak - sub0_player_id = game.submissions[game.submission_order[0]]["player_id"] + sub0_id = game.submissions[game.submission_order[0]]["player_id"] + sub1_id = game.submissions[game.submission_order[1]]["player_id"] game.execute_action(judges[0], "judge_pick_0") game.execute_action(judges[1], "judge_pick_1") - winner = game.get_player_by_id(sub0_player_id) - assert winner.score == 1 # type: ignore[union-attr] + p0 = game.get_player_by_id(sub0_id) + p1 = game.get_player_by_id(sub1_id) + assert p0.score == 1 # type: ignore[union-attr] + assert p1.score == 1 # type: ignore[union-attr] -def test_multi_judge_voted_broadcast(): - game, users = _setup_multi_judge(num_judges=2, num_players=4) +def test_independent_announcement_includes_points(): + game, users = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Independent") for u in users: u.clear_messages() judges = game._get_judges() game.execute_action(judges[0], "judge_pick_0") + game.execute_action(judges[1], "judge_pick_0") all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert any("made their choice" in m for m in all_spoken) + assert any("2 points" in m for m in all_spoken) -def test_single_judge_no_waiting_broadcast(): - game, users = _get_to_judging(num_players=3) - for u in users: - u.clear_messages() - judge = game._get_judges()[0] - game.execute_action(judge, "judge_pick_0") - all_spoken = [m for u in users for m in u.get_spoken_messages()] - # No "made their choice" intermediate broadcast — single judge goes straight to result - assert not any("made their choice" in m for m in all_spoken) +def test_jury_sole_winner_gets_one_point(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Jury") + judges = game._get_judges() + sub0_id = game.submissions[game.submission_order[0]]["player_id"] + game.execute_action(judges[0], "judge_pick_0") + game.execute_action(judges[1], "judge_pick_0") + winner = game.get_player_by_id(sub0_id) + assert winner.score == 1 # type: ignore[union-attr] + + +def test_jury_tie_both_score(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Jury") + judges = game._get_judges() + assert len(game.submissions) >= 2 + sub0_id = game.submissions[game.submission_order[0]]["player_id"] + sub1_id = game.submissions[game.submission_order[1]]["player_id"] + game.execute_action(judges[0], "judge_pick_0") + game.execute_action(judges[1], "judge_pick_1") + p0 = game.get_player_by_id(sub0_id) + p1 = game.get_player_by_id(sub1_id) + assert p0.score == 1 # type: ignore[union-attr] + assert p1.score == 1 # type: ignore[union-attr] + + +def test_jury_single_judge_uses_independent_enforcement(): + # With 1 judge, active_judging_method = Independent regardless of setting + game, _ = _setup_game( + num_players=3, + options=HumanityCardsOptions(num_judges=1, judging_method="Jury"), + ) + for non_judge in game._get_non_judges(): + game.execute_action(non_judge, "toggle_card_0") + game.execute_action(non_judge, "submit_cards") + assert game.active_judging_method == "Independent" + + +def test_random_method_resolves_to_independent_or_jury(): + game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Random") + assert game.active_judging_method in ("Independent", "Jury") def test_wrong_card_count_speaks_error_not_raw_tuple(): From b51558191954050753733f46c0b85a3029c4fe2f Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 01:48:27 -0400 Subject: [PATCH 07/14] fix(humanitycards): remove score from winner announcement Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 2 +- server/locales/en/humanitycards.ftl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index 5153d08f..6ec0291a 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -1104,7 +1104,7 @@ def _announce_and_score_winners(self, scored: list[tuple[str, int]]) -> None: self.current_black_card["text"] if self.current_black_card else "", sub["cards"] if sub else [], ) - self.broadcast_l("hc-winner-announcement", player=player.name, points=points, text=text, score=hcp.score) + self.broadcast_l("hc-winner-announcement", player=player.name, points=points, text=text) def _announce_losing_submissions(self, winner_ids: set[str]) -> None: self.broadcast_l("hc-all-submissions") diff --git a/server/locales/en/humanitycards.ftl b/server/locales/en/humanitycards.ftl index 2becaf92..8db0d893 100644 --- a/server/locales/en/humanitycards.ftl +++ b/server/locales/en/humanitycards.ftl @@ -80,7 +80,7 @@ hc-submission-option = { $text } hc-winner-announcement = { $player } gets { $points } { $points -> [one] point *[other] points -} for { $text }. Score: { $score }. +} for { $text }. hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> [one] point From b78782bd94113b8b94ab095fd8b2fa9635cd46f1 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 02:04:06 -0400 Subject: [PATCH 08/14] Fix CAH edge cases: all-judge mode, self-voting prevention, noise reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove hc-judge-voted broadcast (keep sound only) - Wrap hc-all-submissions heading in guard — only shown when losers exist - Allow num_judges == num_players (all-judge mode): everyone submits AND judges - Self-voting blocked in _judge_pick, _is_judge_pick_enabled, _is_judge_pick_hidden - Update all judge-check guards in submit/toggle/view actions for all-judge bypass - _start_judging, submit completion check, _start_round use _get_submitters() - Bot submission and judging logic handle all-judge mode correctly - Add 8 new tests covering all-judge mode, self-vote prevention, no-losers heading Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 81 +++++++++++------- server/tests/test_humanitycards.py | 127 +++++++++++++++++++++++++---- 2 files changed, 165 insertions(+), 43 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index 6ec0291a..9d1196a6 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -404,12 +404,19 @@ def _get_non_judges(self) -> list[HumanityCardsPlayer]: judge_ids = {j.id for j in self._get_judges()} return [p for p in self.get_active_players() if p.id not in judge_ids] + def _all_players_are_judges(self) -> bool: + return len(self._get_judges()) >= len(self.get_active_players()) + + def _get_submitters(self) -> list[HumanityCardsPlayer]: + """Players expected to submit this round (everyone when all are judges).""" + if self._all_players_are_judges(): + return list(self.get_active_players()) + return self._get_non_judges() + def _select_judges(self) -> None: """Select judge(s) for the current round based on czar_selection option.""" active = self.get_active_players() - num_judges = min(self.options.num_judges, len(active) - 1) # At least 1 non-judge - if num_judges < 1: - num_judges = 1 + num_judges = max(1, min(self.options.num_judges, len(active))) mode = self.options.czar_selection @@ -611,7 +618,7 @@ def _is_toggle_card_enabled(self, player: Player, action_id: str) -> str | None: if player.is_spectator: return "action-spectator" hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return "action-spectator" if hcp.submitted_cards is not None: return "hc-already-submitted" @@ -628,7 +635,7 @@ def _is_toggle_card_hidden(self, player: Player, action_id: str) -> Visibility: if player.is_spectator: return Visibility.HIDDEN hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return Visibility.HIDDEN if hcp.submitted_cards is not None: return Visibility.HIDDEN @@ -662,7 +669,7 @@ def _is_submit_enabled(self, player: Player) -> str | None: if player.is_spectator: return "action-spectator" hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return "action-spectator" if hcp.submitted_cards is not None: return "hc-already-submitted" @@ -676,7 +683,7 @@ def _is_submit_hidden(self, player: Player) -> Visibility: if player.is_spectator: return Visibility.HIDDEN hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return Visibility.HIDDEN if hcp.submitted_cards is not None: return Visibility.HIDDEN @@ -711,6 +718,9 @@ def _is_judge_pick_enabled(self, player: Player, action_id: str) -> str | None: idx = int(action_id.removeprefix("judge_pick_")) if idx >= len(self.submission_order): return "action-not-playing" + sub_idx = self.submission_order[idx] + if sub_idx < len(self.submissions) and self.submissions[sub_idx]["player_id"] == hcp.id: + return "action-not-playing" return None def _is_judge_pick_hidden(self, player: Player, action_id: str) -> Visibility: @@ -722,6 +732,9 @@ def _is_judge_pick_hidden(self, player: Player, action_id: str) -> Visibility: idx = int(action_id.removeprefix("judge_pick_")) if idx >= len(self.submission_order): return Visibility.HIDDEN + sub_idx = self.submission_order[idx] + if sub_idx < len(self.submissions) and self.submissions[sub_idx]["player_id"] == hcp.id: + return Visibility.HIDDEN return Visibility.VISIBLE def _get_judge_pick_label(self, player: Player, action_id: str) -> str: @@ -809,7 +822,7 @@ def _action_whose_turn(self, player: Player, action_id: str) -> None: if self.phase == "submitting": # List who hasn't submitted - waiting = [p.name for p in self._get_non_judges() if p.submitted_cards is None] + waiting = [p.name for p in self._get_submitters() if p.submitted_cards is None] if waiting: user.speak_l("hc-waiting-for", names=", ".join(waiting)) else: @@ -837,7 +850,7 @@ def _is_view_submission_enabled(self, player: Player) -> str | None: if self.status != "playing": return "action-not-playing" hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return "action-spectator" if self.phase != "submitting" and self.phase != "judging": return "action-not-playing" @@ -852,7 +865,7 @@ def _is_view_submission_hidden(self, player: Player) -> Visibility: if self.phase not in ("submitting", "judging"): return Visibility.HIDDEN hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return Visibility.HIDDEN return Visibility.VISIBLE @@ -873,7 +886,7 @@ def _toggle_card(self, player: Player, index: int) -> None: hcp: HumanityCardsPlayer = player # type: ignore if self.phase != "submitting" or hcp.submitted_cards is not None: return - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return if index >= len(hcp.hand): return @@ -1011,7 +1024,7 @@ def _action_submit_cards(self, player: Player, action_id: str) -> None: hcp: HumanityCardsPlayer = player # type: ignore if self.phase != "submitting" or hcp.submitted_cards is not None: return - if self._is_judge(hcp): + if self._is_judge(hcp) and not self._all_players_are_judges(): return required = self.current_black_card["pick"] if self.current_black_card else 1 @@ -1048,9 +1061,9 @@ def _action_submit_cards(self, player: Player, action_id: str) -> None: self.rebuild_all_menus() # Check if all have submitted - non_judges = self._get_non_judges() - submitted_count = sum(1 for p in non_judges if p.submitted_cards is not None) - total = len(non_judges) + submitters = self._get_submitters() + submitted_count = sum(1 for p in submitters if p.submitted_cards is not None) + total = len(submitters) if submitted_count >= total: self._start_judging() @@ -1070,6 +1083,8 @@ def _judge_pick(self, player: Player, pick_index: int) -> None: return picked_player_id = self.submissions[actual_idx]["player_id"] + if picked_player_id == hcp.id: + return # Cannot vote for own submission self.judge_picks[hcp.id] = picked_player_id judges = self._get_judges() @@ -1077,7 +1092,6 @@ def _judge_pick(self, player: Player, pick_index: int) -> None: if judges_still_pending: self.play_sound(f"game_humanitycards/judgechoice{random.randint(1, 3)}.ogg") # nosec B311 - self.broadcast_l("hc-judge-voted", player=hcp.name) self.rebuild_all_menus() return @@ -1107,10 +1121,11 @@ def _announce_and_score_winners(self, scored: list[tuple[str, int]]) -> None: self.broadcast_l("hc-winner-announcement", player=player.name, points=points, text=text) def _announce_losing_submissions(self, winner_ids: set[str]) -> None: + losers = [s for s in self.submissions if s["player_id"] not in winner_ids] + if not losers: + return self.broadcast_l("hc-all-submissions") - for sub in self.submissions: - if sub["player_id"] in winner_ids: - continue + for sub in losers: sub_player = self.get_player_by_id(sub["player_id"]) if sub_player: filled = self._fill_in_blanks( @@ -1333,15 +1348,15 @@ def _start_round(self) -> None: if pick_count > 1: self.broadcast_l("hc-black-card-pick", count=pick_count) - # Tell non-judges to select cards - for p in self._get_non_judges(): + # Tell submitters to select cards + for p in self._get_submitters(): user = self.get_user(p) if user: user.speak_l("hc-select-cards", count=pick_count) # Jolt bots for p in active_players: - if p.is_bot and not self._is_judge(p): + if p.is_bot and (not self._is_judge(p) or self._all_players_are_judges()): BotHelper.jolt_bot(p, ticks=random.randint(20, 40)) # nosec B311 self.rebuild_all_menus() @@ -1352,7 +1367,7 @@ def _start_judging(self) -> None: # Collect submissions self.submissions = [] - for p in self._get_non_judges(): + for p in self._get_submitters(): if p.submitted_cards is not None: self.submissions.append( { @@ -1399,7 +1414,7 @@ def _end_game(self, winner: HumanityCardsPlayer) -> None: def bot_think(self, player: HumanityCardsPlayer) -> str | None: """Bot AI decision making.""" - if self.phase == "submitting" and not self._is_judge(player): + if self.phase == "submitting" and (not self._is_judge(player) or self._all_players_are_judges()): if player.submitted_cards is not None: return None required = self.current_black_card["pick"] if self.current_black_card else 1 @@ -1443,7 +1458,9 @@ def _process_submission_bots(self) -> None: if not player.is_bot or player.is_spectator: continue hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp) or hcp.submitted_cards is not None: + if self._is_judge(hcp) and not self._all_players_are_judges(): + continue + if hcp.submitted_cards is not None: continue BotHelper.process_bot_action( @@ -1468,10 +1485,18 @@ def _process_judging_bots(self) -> None: self.execute_action(judge, action_id) continue - # Bot judge picks a random submission + # Bot judge picks a random submission (skip own in all-judge mode) if self.submission_order: - pick = random.randint(0, len(self.submission_order) - 1) # nosec B311 - judge.bot_pending_action = f"judge_pick_{pick}" + valid_picks = list(range(len(self.submission_order))) + if self._all_players_are_judges(): + valid_picks = [ + i for i in valid_picks + if self.submission_order[i] < len(self.submissions) + and self.submissions[self.submission_order[i]]["player_id"] != judge.id + ] + if valid_picks: + pick = random.choice(valid_picks) # nosec B311 + judge.bot_pending_action = f"judge_pick_{pick}" # ========================================================================== # Game result diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py index 1e92eba2..577c37d4 100644 --- a/server/tests/test_humanitycards.py +++ b/server/tests/test_humanitycards.py @@ -4,6 +4,7 @@ from server.core.users.bot import Bot from server.core.users.test_user import MockUser +from server.game_utils.actions import Visibility from server.games.humanitycards.game import HumanityCardsGame, HumanityCardsOptions @@ -126,11 +127,11 @@ def test_rotating_judge_advances_each_round(): assert first_judge_id != second_judge_id -def test_judge_count_never_exceeds_active_minus_one(): - game, _ = _setup_game(num_players=3, options=HumanityCardsOptions(num_judges=3)) - # 3 players, max judges = active - 1 = 2 +def test_judge_count_capped_at_active_player_count(): + game, _ = _setup_game(num_players=3, options=HumanityCardsOptions(num_judges=5)) + # num_judges > active players: capped at active count (all-judge mode) judges = game._get_judges() - assert len(judges) <= len(game.get_active_players()) - 1 + assert len(judges) == len(game.get_active_players()) def test_random_judge_selection_picks_valid_player(): @@ -629,24 +630,17 @@ def test_multi_judge_cannot_vote_twice(): assert game.judge_picks == first_picks -def test_multi_judge_voted_broadcast(): +def test_multi_judge_voted_plays_sound(): game, users = _setup_multi_judge(num_judges=2, num_players=4) for u in users: u.clear_messages() judges = game._get_judges() game.execute_action(judges[0], "judge_pick_0") - all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert any("made their choice" in m for m in all_spoken) - - -def test_single_judge_no_waiting_broadcast(): - game, users = _get_to_judging(num_players=3) - for u in users: - u.clear_messages() - judge = game._get_judges()[0] - game.execute_action(judge, "judge_pick_0") + # No speech broadcast for intermediate vote — sound only all_spoken = [m for u in users for m in u.get_spoken_messages()] assert not any("made their choice" in m for m in all_spoken) + all_sounds = [s for u in users for s in u.get_sounds_played()] + assert any("judgechoice" in s for s in all_sounds) # ========================================================================== @@ -804,3 +798,106 @@ def test_bot_game_all_players_score_tracked(): game.on_tick() total_score = sum(p.score for p in game.get_active_players()) # type: ignore[union-attr] assert total_score >= opts.winning_score + + +# ========================================================================== +# All-judge mode +# ========================================================================== + + +def _setup_all_judge(num_players: int = 3) -> tuple[HumanityCardsGame, list]: + """Set up game in submitting phase with everyone as judge.""" + opts = HumanityCardsOptions(num_judges=num_players) + game, users = _setup_game(num_players=num_players, options=opts) + return game, users + + +def test_all_judge_mode_everyone_is_judge(): + game, _ = _setup_all_judge(num_players=3) + assert game._all_players_are_judges() + assert len(game._get_judges()) == 3 + + +def test_all_judge_mode_submitters_are_all_players(): + game, _ = _setup_all_judge(num_players=3) + submitters = game._get_submitters() + active = game.get_active_players() + assert len(submitters) == len(active) + + +def test_all_judge_mode_players_can_submit(): + game, users = _setup_all_judge(num_players=3) + for p in game.get_active_players(): + assert game._is_toggle_card_enabled(p, "toggle_card_0") is None + assert game._is_submit_enabled(p) is None + + +def test_all_judge_mode_submitting_progresses_to_judging(): + game, _ = _setup_all_judge(num_players=3) + for p in game.get_active_players(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + assert game.phase == "judging" + assert len(game.submissions) == 3 + + +def test_all_judge_mode_self_vote_hidden(): + game, _ = _setup_all_judge(num_players=3) + for p in game.get_active_players(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + assert game.phase == "judging" + judges = game._get_judges() + for judge in judges: + # Find which pick index is their own submission + for i, sub_idx in enumerate(game.submission_order): + sub = game.submissions[sub_idx] + if sub["player_id"] == judge.id: + vis = game._is_judge_pick_hidden(judge, f"judge_pick_{i}") + assert vis == Visibility.HIDDEN, f"{judge.name} can see own submission at pick_{i}" + + +def test_all_judge_mode_self_vote_blocked_in_action(): + game, _ = _setup_all_judge(num_players=3) + for p in game.get_active_players(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + assert game.phase == "judging" + judges = game._get_judges() + for judge in judges: + for i, sub_idx in enumerate(game.submission_order): + sub = game.submissions[sub_idx] + if sub["player_id"] == judge.id: + game._judge_pick(judge, i) + assert judge.id not in game.judge_picks, f"{judge.name} voted for self" + + +def test_no_losing_submissions_heading_when_all_are_winners(): + """_announce_losing_submissions should not broadcast the heading if there are no losers.""" + game, users = _setup_game(num_players=3) + # Inject fake submissions + game.submissions = [ + {"player_id": "p1", "cards": ["foo"]}, + {"player_id": "p2", "cards": ["bar"]}, + ] + game.current_black_card = _make_black("_ question", pick=1) + for u in users: + u.clear_messages() + # All submission player_ids are winners — no losers + game._announce_losing_submissions({"p1", "p2"}) + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert not any("other submissions" in m.lower() for m in all_spoken) + + +def test_all_judge_bot_game_completes(): + opts = HumanityCardsOptions(winning_score=2, num_judges=3) + game = HumanityCardsGame(options=opts) + game._build_decks = lambda: _inject_decks(game, white_count=500, black_count=100) # type: ignore[method-assign] + for i in range(3): + game.add_player(f"Bot{i}", Bot(f"Bot{i}")) + game.on_start() + for _ in range(200_000): + if game.status == "finished": + break + game.on_tick() + assert game.status == "finished" From 7804d021ae16f00caeb9e20e222107c4a143251b Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 02:13:56 -0400 Subject: [PATCH 09/14] Fix CAH judge announcement grammar, all-judge silence, judging whose_turn - hc-judge-is now uses $names (pre-formatted list) instead of $player/$others - _format_names helper: proper Oxford comma for 3+ names, "A and B" for 2 - Skip czar broadcast and personal "you are judge" messages in all-judge mode - _action_whose_turn during judging: list pending judges, not submission state - Add hc-waiting-for-judges FTL key for judging phase whose_turn response - 4 new tests: grammar (2-judge, 3-judge), all-judge silence, judging whose_turn Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 55 +++++++++++++++++------------ server/locales/en/humanitycards.ftl | 10 +++--- server/tests/test_humanitycards.py | 54 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index 9d1196a6..f4e09b1d 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -407,6 +407,15 @@ def _get_non_judges(self) -> list[HumanityCardsPlayer]: def _all_players_are_judges(self) -> bool: return len(self._get_judges()) >= len(self.get_active_players()) + @staticmethod + def _format_names(names: list[str]) -> str: + """Format a list of names as a grammatically correct series.""" + if len(names) == 1: + return names[0] + if len(names) == 2: + return f"{names[0]} and {names[1]}" + return ", ".join(names[:-1]) + f", and {names[-1]}" + def _get_submitters(self) -> list[HumanityCardsPlayer]: """Players expected to submit this round (everyone when all are judges).""" if self._all_players_are_judges(): @@ -805,30 +814,32 @@ def _action_whose_judge(self, player: Player, action_id: str) -> None: if not user: return judges = self._get_judges() - if len(judges) == 1: - user.speak_l("hc-judge-is", player=judges[0].name, count=1, others="") - elif judges: - others = ", ".join(j.name for j in judges[1:]) - user.speak_l("hc-judge-is", player=judges[0].name, count=len(judges), others=others) + if judges: + names = self._format_names([j.name for j in judges]) + user.speak_l("hc-judge-is", names=names, count=len(judges)) def _action_whose_turn(self, player: Player, action_id: str) -> None: - """Override default whose_turn to show submission status.""" + """Override default whose_turn to show submission/judging status.""" user = self.get_user(player) if not user: return - judges = self._get_judges() - judge_names = ", ".join(j.name for j in judges) - if self.phase == "submitting": - # List who hasn't submitted waiting = [p.name for p in self._get_submitters() if p.submitted_cards is None] if waiting: - user.speak_l("hc-waiting-for", names=", ".join(waiting)) + user.speak_l("hc-waiting-for", names=self._format_names(waiting)) else: - user.speak_l("hc-all-submitted-waiting-judge", judge=judge_names) + judges = self._get_judges() + user.speak_l( + "hc-all-submitted-waiting-judge", + judge=self._format_names([j.name for j in judges]), + ) elif self.phase == "judging": - user.speak_l("hc-all-submitted-waiting-judge", judge=judge_names) + pending = [j for j in self._get_judges() if j.id not in self.judge_picks] + if pending: + user.speak_l("hc-waiting-for-judges", names=self._format_names([j.name for j in pending])) + else: + user.speak_l("game-no-turn") else: user.speak_l("game-no-turn") @@ -1330,17 +1341,15 @@ def _start_round(self) -> None: # Announce round self.broadcast_l("hc-round-start", round=self.round) - # Announce judge(s) + # Announce judge(s) — skip in all-judge mode (everyone knows) judges = self._get_judges() - if len(judges) == 1: - self.broadcast_l("hc-judge-is", player=judges[0].name, count=1, others="") - else: - others = ", ".join(j.name for j in judges[1:]) - self.broadcast_l("hc-judge-is", player=judges[0].name, count=len(judges), others=others) - for judge in judges: - user = self.get_user(judge) - if user: - user.speak_l("hc-you-are-judge") + if not self._all_players_are_judges(): + names = self._format_names([j.name for j in judges]) + self.broadcast_l("hc-judge-is", names=names, count=len(judges)) + for judge in judges: + user = self.get_user(judge) + if user: + user.speak_l("hc-you-are-judge") # Announce black card black_text = self._speech_friendly_black(self.current_black_card["text"]) diff --git a/server/locales/en/humanitycards.ftl b/server/locales/en/humanitycards.ftl index 8db0d893..b943a8e9 100644 --- a/server/locales/en/humanitycards.ftl +++ b/server/locales/en/humanitycards.ftl @@ -41,13 +41,15 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } is the Card Czar. + *[other] { $names } are the Card Czars. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. +hc-waiting-for-judges = Waiting for { $names } to judge. + # Black card hc-black-card = The prompt is: { $text } hc-black-card-pick = Pick { $count }. diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py index 577c37d4..3c988889 100644 --- a/server/tests/test_humanitycards.py +++ b/server/tests/test_humanitycards.py @@ -901,3 +901,57 @@ def test_all_judge_bot_game_completes(): break game.on_tick() assert game.status == "finished" + + +# ========================================================================== +# Grammar and announcement fixes +# ========================================================================== + + +def test_judge_announcement_two_judges_grammar(): + game, users = _setup_game(num_players=4, options=HumanityCardsOptions(num_judges=2)) + all_spoken = [m for u in users for m in u.get_spoken_messages()] + judge_names = [j.name for j in game._get_judges()] + # Should be "Alice and Bob are the Card Czars" — no trailing "and X, Y" + judge_announce = [m for m in all_spoken if "Card Czar" in m] + assert judge_announce, "no judge announcement found" + msg = judge_announce[0] + assert judge_names[0] in msg + assert judge_names[1] in msg + assert msg.count("and") == 1 + + +def test_judge_announcement_three_judges_oxford_comma(): + game, users = _setup_game(num_players=4, options=HumanityCardsOptions(num_judges=3)) + all_spoken = [m for u in users for m in u.get_spoken_messages()] + judge_names = [j.name for j in game._get_judges()] + judge_announce = [m for m in all_spoken if "Card Czar" in m] + assert judge_announce + msg = judge_announce[0] + # All names present, with Oxford comma pattern + for name in judge_names: + assert name in msg + assert ", and " in msg + + +def test_all_judge_mode_no_czar_announcement(): + game, users = _setup_all_judge(num_players=3) + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert not any("Card Czar" in m for m in all_spoken) + + +def test_whose_turn_judging_lists_pending_judges(): + game, users = _setup_multi_judge(num_judges=2, num_players=4) + judges = game._get_judges() + # First judge votes + game.execute_action(judges[0], "judge_pick_0") + assert game.phase == "judging" + # Use a non-judge player to call whose_turn + non_judge = game._get_non_judges()[0] + non_judge_user = next(u for u in users if u.username == non_judge.name) + non_judge_user.clear_messages() + game._action_whose_turn(non_judge, "whose_turn") + spoken = non_judge_user.get_spoken_messages() + # Should mention the pending judge, not "submitted" + assert any(judges[1].name in m for m in spoken) + assert not any("submitted" in m.lower() for m in spoken) From 86cb5a2785e1782b0e972224ee597f5620973667 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 02:22:50 -0400 Subject: [PATCH 10/14] Strip trailing period from winner announcement text to avoid double period Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index f4e09b1d..454d316c 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -1128,7 +1128,7 @@ def _announce_and_score_winners(self, scored: list[tuple[str, int]]) -> None: text = self._fill_in_blanks( self.current_black_card["text"] if self.current_black_card else "", sub["cards"] if sub else [], - ) + ).rstrip(".") self.broadcast_l("hc-winner-announcement", player=player.name, points=points, text=text) def _announce_losing_submissions(self, winner_ids: set[str]) -> None: From 21040d3d57b872eaa885ce96103a5a56f2f2cad5 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 02:25:01 -0400 Subject: [PATCH 11/14] Extract CAH bot logic to bot.py, matching pattern used by farkle/crazyeights - New bot.py: bot_think, _think_submitting, _think_judging - game.py: bot_think delegates to _bot_think(self, player) - _process_judging_bots now uses BotHelper.process_bot_action (same as submission) - Removed inline duplicate of judging selection logic Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/bot.py | 58 ++++++++++++++++++++++++++++++ server/games/humanitycards/game.py | 55 ++++------------------------ 2 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 server/games/humanitycards/bot.py diff --git a/server/games/humanitycards/bot.py b/server/games/humanitycards/bot.py new file mode 100644 index 00000000..b5a0dd43 --- /dev/null +++ b/server/games/humanitycards/bot.py @@ -0,0 +1,58 @@ +"""Heuristic bot logic for Humanity Cards.""" + +from __future__ import annotations + +import random +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .game import HumanityCardsGame, HumanityCardsPlayer + + +def bot_think(game: "HumanityCardsGame", player: "HumanityCardsPlayer") -> str | None: + if game.phase == "submitting": + return _think_submitting(game, player) + if game.phase == "judging": + return _think_judging(game, player) + return None + + +def _think_submitting(game: "HumanityCardsGame", player: "HumanityCardsPlayer") -> str | None: + if game._is_judge(player) and not game._all_players_are_judges(): + return None + if player.submitted_cards is not None: + return None + + required = game.current_black_card["pick"] if game.current_black_card else 1 + + if len(player.selected_indices) < required: + available = [i for i in range(len(player.hand)) if i not in player.selected_indices] + if available: + return f"toggle_card_{random.choice(available)}" # nosec B311 + + if len(player.selected_indices) == required: + return "submit_cards" + + return None + + +def _think_judging(game: "HumanityCardsGame", player: "HumanityCardsPlayer") -> str | None: + if not game._is_judge(player): + return None + if player.id in game.judge_picks: + return None + if not game.submission_order: + return None + + valid_picks = list(range(len(game.submission_order))) + if game._all_players_are_judges(): + valid_picks = [ + i for i in valid_picks + if game.submission_order[i] < len(game.submissions) + and game.submissions[game.submission_order[i]]["player_id"] != player.id + ] + + if not valid_picks: + return None + + return f"judge_pick_{random.choice(valid_picks)}" # nosec B311 diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index 454d316c..bec67c40 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -15,6 +15,7 @@ from ..registry import register_game from ...game_utils.actions import Action, ActionSet, Visibility from ...game_utils.bot_helper import BotHelper +from .bot import bot_think as _bot_think from ...game_utils.game_result import GameResult, PlayerResult from ...game_utils.options import ( IntOption, @@ -1422,24 +1423,7 @@ def _end_game(self, winner: HumanityCardsPlayer) -> None: # ========================================================================== def bot_think(self, player: HumanityCardsPlayer) -> str | None: - """Bot AI decision making.""" - if self.phase == "submitting" and (not self._is_judge(player) or self._all_players_are_judges()): - if player.submitted_cards is not None: - return None - required = self.current_black_card["pick"] if self.current_black_card else 1 - - # Select random cards if not enough selected - if len(player.selected_indices) < required: - available = [i for i in range(len(player.hand)) if i not in player.selected_indices] - if available: - pick = random.choice(available) # nosec B311 - return f"toggle_card_{pick}" - - # Submit when we have enough - if len(player.selected_indices) == required: - return "submit_cards" - - return None + return _bot_think(self, player) def on_tick(self) -> None: """Called every tick.""" @@ -1462,16 +1446,10 @@ def on_tick(self) -> None: self._process_judging_bots() def _process_submission_bots(self) -> None: - """Process all bot actions during submission phase.""" for player in self.players: if not player.is_bot or player.is_spectator: continue hcp: HumanityCardsPlayer = player # type: ignore - if self._is_judge(hcp) and not self._all_players_are_judges(): - continue - if hcp.submitted_cards is not None: - continue - BotHelper.process_bot_action( player, think_fn=lambda p=hcp: self.bot_think(p), @@ -1479,33 +1457,14 @@ def _process_submission_bots(self) -> None: ) def _process_judging_bots(self) -> None: - """Process judge bot actions during judging phase.""" for judge in self._get_judges(): if not judge.is_bot: continue - - if judge.bot_think_ticks > 0: - judge.bot_think_ticks -= 1 - continue - - if judge.bot_pending_action: - action_id = judge.bot_pending_action - judge.bot_pending_action = None - self.execute_action(judge, action_id) - continue - - # Bot judge picks a random submission (skip own in all-judge mode) - if self.submission_order: - valid_picks = list(range(len(self.submission_order))) - if self._all_players_are_judges(): - valid_picks = [ - i for i in valid_picks - if self.submission_order[i] < len(self.submissions) - and self.submissions[self.submission_order[i]]["player_id"] != judge.id - ] - if valid_picks: - pick = random.choice(valid_picks) # nosec B311 - judge.bot_pending_action = f"judge_pick_{pick}" + BotHelper.process_bot_action( + judge, + think_fn=lambda j=judge: self.bot_think(j), + execute_fn=lambda action_id, j=judge: self.execute_action(j, action_id), + ) # ========================================================================== # Game result From e2dbdf8aa4b384aed3500631814d360599dfddec Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 02:47:22 -0400 Subject: [PATCH 12/14] Use team_manager for CAH scoring instead of custom overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setup_teams("individual") in on_start — one team per player - add_to_team_score when awarding points in _announce_and_score_winners - Remove custom _is_check_scores_enabled, _is_check_scores_detailed_enabled, _action_check_scores, _action_check_scores_detailed — base class handles all - Update score display tests to match base class output format Co-Authored-By: Claude Sonnet 4.6 --- server/games/humanitycards/game.py | 36 +++++------------------------- server/tests/test_humanitycards.py | 16 +++++++++---- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index bec67c40..1845baa7 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -1125,6 +1125,7 @@ def _announce_and_score_winners(self, scored: list[tuple[str, int]]) -> None: continue hcp: HumanityCardsPlayer = player # type: ignore hcp.score += points + self._team_manager.add_to_team_score(player.name, points) sub = next((s for s in self.submissions if s["player_id"] == player_id), None) text = self._fill_in_blanks( self.current_black_card["text"] if self.current_black_card else "", @@ -1233,37 +1234,6 @@ def _action_view_submission(self, player: Player, action_id: str) -> None: else: user.speak_l("hc-select-cards-first") - # ========================================================================== - # Score overrides (CAH uses player.score, not team_manager) - # ========================================================================== - - def _is_check_scores_enabled(self, player: Player) -> str | None: - if self.status != "playing": - return "action-not-playing" - return None - - def _is_check_scores_detailed_enabled(self, player: Player) -> str | None: - if self.status != "playing": - return "action-not-playing" - return None - - def _action_check_scores(self, player: Player, action_id: str) -> None: - user = self.get_user(player) - if not user: - return - for p in sorted(self.get_active_players(), key=lambda p: p.score, reverse=True): # type: ignore - user.speak_l("hc-score-line", player=p.name, score=p.score) # type: ignore - - def _action_check_scores_detailed(self, player: Player, action_id: str) -> None: - user = self.get_user(player) - if not user: - return - lines = [ - Localization.get(user.locale, "hc-score-line", player=p.name, score=p.score) # type: ignore - for p in sorted(self.get_active_players(), key=lambda p: p.score, reverse=True) # type: ignore - ] - self.status_box(player, lines) - # ========================================================================== # Game lifecycle # ========================================================================== @@ -1281,6 +1251,10 @@ def on_start(self) -> None: active_players = self.get_active_players() + # Set up individual teams for score tracking (S / Shift+S) + self._team_manager.team_mode = "individual" + self._team_manager.setup_teams([p.name for p in active_players]) + # Check we have enough cards total_whites_needed = len(active_players) * self.options.hand_size if len(self.white_deck) < total_whites_needed: diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py index 3c988889..b0c814aa 100644 --- a/server/tests/test_humanitycards.py +++ b/server/tests/test_humanitycards.py @@ -501,11 +501,11 @@ def test_check_scores_includes_score_values(): game, users = _setup_game(num_players=3) player0 = _get_player(game, 0) player0.score = 5 # type: ignore[union-attr] + game._team_manager.add_to_team_score(player0.name, 5) user0 = next(u for u in users if u.username == player0.name) user0.clear_messages() game.execute_action(player0, "check_scores") spoken = user0.get_spoken_messages() - # Score "5" or "5 points" should appear assert any("5" in m for m in spoken) @@ -515,12 +515,17 @@ def test_check_scores_ordered_descending(): active[0].score = 5 # type: ignore[union-attr] active[1].score = 3 # type: ignore[union-attr] active[2].score = 1 # type: ignore[union-attr] + # Sync scores into team_manager so check_scores reflects them + for p in active: + game._team_manager.add_to_team_score(p.name, p.score) # type: ignore[union-attr] user0 = next(u for u in users if u.username == active[0].name) user0.clear_messages() game.execute_action(active[0], "check_scores") spoken = user0.get_spoken_messages() - # First spoken message should mention the highest scorer - assert active[0].name in spoken[0] + all_text = " ".join(spoken) + # Highest scorer's name should appear, and their score should be present + assert active[0].name in all_text + assert "5" in all_text def test_check_scores_speaks_all_players(): @@ -530,7 +535,10 @@ def test_check_scores_speaks_all_players(): user0.clear_messages() game.execute_action(player0, "check_scores") spoken = user0.get_spoken_messages() - assert len(spoken) == 3 # One line per player + # Base class may combine into one message or speak per team — all player names must appear + all_text = " ".join(spoken) + for p in game.get_active_players(): + assert p.name in all_text # ========================================================================== From 208b8f3e83c8ae9acdc96ab5cbad56951a33d9dc Mon Sep 17 00:00:00 2001 From: Jage9 Date: Sun, 19 Apr 2026 02:52:56 -0400 Subject: [PATCH 13/14] =?UTF-8?q?Trim=20CAH=20tests:=2077=20=E2=86=92=2052?= =?UTF-8?q?=20tests,=20965=20=E2=86=92=20495=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove: options defaults, duplicate utility tests, sound-only tests, absence-of-removed-feature tests, redundant score display tests, duplicate judge announcement checks, internal callback tests, tests fully covered by bot game. Collapse related single-case tests. Co-Authored-By: Claude Sonnet 4.6 --- server/tests/test_humanitycards.py | 610 ++++++----------------------- 1 file changed, 128 insertions(+), 482 deletions(-) diff --git a/server/tests/test_humanitycards.py b/server/tests/test_humanitycards.py index b0c814aa..ed22efcd 100644 --- a/server/tests/test_humanitycards.py +++ b/server/tests/test_humanitycards.py @@ -1,7 +1,5 @@ """Tests for Humanity Cards (Cards Against Humanity) game.""" -import pytest - from server.core.users.bot import Bot from server.core.users.test_user import MockUser from server.game_utils.actions import Visibility @@ -22,7 +20,6 @@ def _make_black(text: str = "Why is _ so funny?", pick: int = 1) -> dict: def _inject_decks(game: HumanityCardsGame, white_count: int = 200, black_count: int = 50) -> None: - """Replace decks with deterministic test cards.""" game.white_deck = _make_white(white_count) game.black_deck = [_make_black(f"Question {i} _") for i in range(black_count)] game.white_discard = [] @@ -32,7 +29,6 @@ def _inject_decks(game: HumanityCardsGame, white_count: int = 200, black_count: def _setup_game( num_players: int = 3, options: HumanityCardsOptions | None = None, - use_bots: bool = False, ) -> tuple[HumanityCardsGame, list[MockUser]]: opts = options or HumanityCardsOptions() game = HumanityCardsGame(options=opts) @@ -40,23 +36,31 @@ def _setup_game( users = [] for i in range(num_players): name = f"Player{i}" - if use_bots: - user: MockUser | Bot = Bot(name) - else: - user = MockUser(name) + user = MockUser(name) game.add_player(name, user) - if not use_bots: - users.append(user) # type: ignore[arg-type] + users.append(user) game.on_start() return game, users -def _get_player(game: HumanityCardsGame, index: int): - return game.get_active_players()[index] +def _get_to_judging(num_players: int = 3, options: HumanityCardsOptions | None = None): + game, users = _setup_game(num_players=num_players, options=options) + for p in game._get_submitters(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + assert game.phase == "judging" + return game, users + + +def _setup_multi_judge(num_judges: int = 2, num_players: int = 4, judging_method: str = "Independent"): + return _get_to_judging( + num_players=num_players, + options=HumanityCardsOptions(num_judges=num_judges, judging_method=judging_method), + ) # ========================================================================== -# Metadata & options +# Metadata # ========================================================================== @@ -68,16 +72,8 @@ def test_game_metadata(): assert game.get_max_players() >= 6 -def test_options_defaults(): - opts = HumanityCardsOptions() - assert opts.winning_score == 7 - assert opts.hand_size == 10 - assert opts.czar_selection == "Rotating" - assert opts.num_judges == 1 - - # ========================================================================== -# Game startup +# Startup # ========================================================================== @@ -104,7 +100,6 @@ def test_black_card_dealt_on_start(): game, _ = _setup_game() assert game.current_black_card is not None assert "text" in game.current_black_card - assert "pick" in game.current_black_card # ========================================================================== @@ -114,31 +109,25 @@ def test_black_card_dealt_on_start(): def test_one_judge_on_start(): game, _ = _setup_game() - judges = game._get_judges() - assert len(judges) == 1 + assert len(game._get_judges()) == 1 def test_rotating_judge_advances_each_round(): game, _ = _setup_game(num_players=4) - first_judge_id = game._get_judges()[0].id - # Simulate completing a round by triggering next round + first_id = game._get_judges()[0].id game._start_round() - second_judge_id = game._get_judges()[0].id - assert first_judge_id != second_judge_id + assert game._get_judges()[0].id != first_id def test_judge_count_capped_at_active_player_count(): game, _ = _setup_game(num_players=3, options=HumanityCardsOptions(num_judges=5)) - # num_judges > active players: capped at active count (all-judge mode) - judges = game._get_judges() - assert len(judges) == len(game.get_active_players()) + assert len(game._get_judges()) == len(game.get_active_players()) def test_random_judge_selection_picks_valid_player(): game, _ = _setup_game(options=HumanityCardsOptions(czar_selection="Random")) - judges = game._get_judges() active_ids = {p.id for p in game.get_active_players()} - for j in judges: + for j in game._get_judges(): assert j.id in active_ids @@ -149,39 +138,26 @@ def test_winner_judge_selection_uses_last_winner(): active = game.get_active_players() game.last_winner_index = 2 game._start_round() - judges = game._get_judges() - assert judges[0].id == active[2].id + assert game._get_judges()[0].id == active[2].id def test_judge_personal_announcement_spoken(): game, users = _setup_game(num_players=3) judge = game._get_judges()[0] judge_user = next(u for u in users if u.username == judge.name) - spoken = judge_user.get_spoken_messages() - assert any("Card Czar" in m for m in spoken) + assert any("Card Czar" in m for m in judge_user.get_spoken_messages()) # ========================================================================== -# Utility methods +# Utility # ========================================================================== -def test_fill_in_blanks_single(): +def test_fill_in_blanks(): game, _ = _setup_game() - result = game._fill_in_blanks("I love _.", ["cats"]) - assert result == "I love cats." - - -def test_fill_in_blanks_multiple(): - game, _ = _setup_game() - result = game._fill_in_blanks("_ meets _.", ["Alice", "Bob"]) - assert result == "Alice meets Bob." - - -def test_fill_in_blanks_no_blank_appends(): - game, _ = _setup_game() - result = game._fill_in_blanks("Why?", ["Because reasons"]) - assert result == "Why? Because reasons" + assert game._fill_in_blanks("I love _.", ["cats"]) == "I love cats." + assert game._fill_in_blanks("_ meets _.", ["Alice", "Bob"]) == "Alice meets Bob." + assert game._fill_in_blanks("Why?", ["Because"]) == "Why? Because" def test_speech_friendly_black_replaces_underscore(): @@ -189,11 +165,6 @@ def test_speech_friendly_black_replaces_underscore(): assert game._speech_friendly_black("I love _.") == "I love blank." -def test_speech_friendly_black_multiple(): - game, _ = _setup_game() - assert game._speech_friendly_black("_ and _.") == "blank and blank." - - # ========================================================================== # Deck reshuffle # ========================================================================== @@ -201,12 +172,10 @@ def test_speech_friendly_black_multiple(): def test_white_deck_reshuffles_from_discard(): game, _ = _setup_game() - # Drain white deck game.white_deck = [] game.white_discard = _make_white(5, start=100) drawn = game._draw_white(3) assert len(drawn) == 3 - # Remaining discard minus drawn cards assert len(game.white_deck) + len(drawn) == 5 @@ -225,16 +194,7 @@ def test_black_deck_reshuffles_from_discard(): game, _ = _setup_game() game.black_deck = [] game.black_discard = [_make_black("Test _ card") for _ in range(3)] - card = game._draw_black() - assert card is not None - - -def test_draw_white_returns_empty_list_when_no_cards(): - game, _ = _setup_game() - game.white_deck = [] - game.white_discard = [] - drawn = game._draw_white(5) - assert drawn == [] + assert game._draw_black() is not None # ========================================================================== @@ -242,15 +202,7 @@ def test_draw_white_returns_empty_list_when_no_cards(): # ========================================================================== -def test_toggle_card_selects_card(): - game, users = _setup_game() - non_judge = game._get_non_judges()[0] - assert len(non_judge.selected_indices) == 0 - game.execute_action(non_judge, "toggle_card_0") - assert 0 in non_judge.selected_indices - - -def test_toggle_card_deselects_card(): +def test_toggle_card_selects_and_deselects(): game, _ = _setup_game() non_judge = game._get_non_judges()[0] game.execute_action(non_judge, "toggle_card_0") @@ -266,77 +218,42 @@ def test_judge_cannot_toggle_cards(): assert 0 not in judge.selected_indices # type: ignore[union-attr] -def test_card_toggle_plays_select_sound(): - game, users = _setup_game() - non_judge = game._get_non_judges()[0] - non_judge_user = next(u for u in users if u.username == non_judge.name) - non_judge_user.clear_messages() - game.execute_action(non_judge, "toggle_card_0") - sounds = non_judge_user.get_sounds_played() - assert any("cardselect" in s for s in sounds) - - -def test_card_toggle_plays_unselect_sound(): - game, users = _setup_game() - non_judge = game._get_non_judges()[0] - non_judge_user = next(u for u in users if u.username == non_judge.name) - game.execute_action(non_judge, "toggle_card_0") - non_judge_user.clear_messages() - game.execute_action(non_judge, "toggle_card_0") - sounds = non_judge_user.get_sounds_played() - assert any("cardunselect" in s for s in sounds) - - # ========================================================================== # Submission # ========================================================================== -def test_submit_cards_removes_from_hand(): - game, _ = _setup_game() - non_judge = game._get_non_judges()[0] - hand_size_before = len(non_judge.hand) - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - assert non_judge.submitted_cards is not None - assert len(non_judge.hand) == hand_size_before - 1 - - -def test_submit_cards_records_submission(): +def test_submit_removes_card_from_hand_and_records(): game, _ = _setup_game() non_judge = game._get_non_judges()[0] expected_text = non_judge.hand[0]["text"] + hand_size = len(non_judge.hand) game.execute_action(non_judge, "toggle_card_0") game.execute_action(non_judge, "submit_cards") assert non_judge.submitted_cards == [expected_text] + assert len(non_judge.hand) == hand_size - 1 def test_submit_wrong_count_rejected(): game, users = _setup_game() - # Force a pick-2 black card game.current_black_card = _make_black("_ loves _ forever.", pick=2) non_judge = game._get_non_judges()[0] non_judge_user = next(u for u in users if u.username == non_judge.name) - # Select only 1 card, need 2 game.execute_action(non_judge, "toggle_card_0") non_judge_user.clear_messages() game.execute_action(non_judge, "submit_cards") assert non_judge.submitted_cards is None - spoken = non_judge_user.get_spoken_messages() - assert any("2" in m for m in spoken) + assert any("2" in m for m in non_judge_user.get_spoken_messages()) def test_submit_already_submitted_rejected(): - game, users = _setup_game() + game, _ = _setup_game() non_judge = game._get_non_judges()[0] - non_judge_user = next(u for u in users if u.username == non_judge.name) game.execute_action(non_judge, "toggle_card_0") game.execute_action(non_judge, "submit_cards") - submission_after_first = list(non_judge.submitted_cards) # type: ignore[arg-type] - non_judge_user.clear_messages() - # Try to submit again (no selected cards, already submitted) + first = list(non_judge.submitted_cards) # type: ignore[arg-type] game.execute_action(non_judge, "submit_cards") - assert non_judge.submitted_cards == submission_after_first + assert non_judge.submitted_cards == first def test_judge_cannot_submit(): @@ -349,26 +266,13 @@ def test_judge_cannot_submit(): def test_all_submit_triggers_judging_phase(): game, _ = _setup_game(num_players=3) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") + for p in game._get_non_judges(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") assert game.phase == "judging" -def test_submission_no_progress_broadcast(): - # Progress broadcast was removed — sound only, no speech - game, users = _setup_game(num_players=3) - for u in users: - u.clear_messages() - non_judge = game._get_non_judges()[0] - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - all_spoken = [m for u in users for m in u.get_spoken_messages()] - # Only the submitter hears "You submitted your cards." — no "N of M" broadcast - assert not any("of" in m and "player" in m.lower() for m in all_spoken) - - -def test_pick_two_black_card_requires_two_submissions(): +def test_pick_two_black_card_accepts_two_cards(): game, _ = _setup_game() game.current_black_card = _make_black("_ with _ always.", pick=2) non_judge = game._get_non_judges()[0] @@ -384,38 +288,21 @@ def test_pick_two_black_card_requires_two_submissions(): # ========================================================================== -def _get_to_judging(num_players: int = 3): - game, users = _setup_game(num_players=num_players) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - assert game.phase == "judging" - return game, users - - -def test_judge_pick_awards_point(): +def test_judge_pick_awards_point_and_ends_round(): game, _ = _get_to_judging() judge = game._get_judges()[0] - winner_before = {p.id: p.score for p in game.get_active_players()} # type: ignore[union-attr] - game.execute_action(judge, "judge_pick_0") winner_id = game.submissions[game.submission_order[0]]["player_id"] - winner = game.get_player_by_id(winner_id) - assert winner.score == winner_before[winner_id] + 1 # type: ignore[union-attr] - - -def test_judge_pick_transitions_to_round_end(): - game, _ = _get_to_judging() - judge = game._get_judges()[0] game.execute_action(judge, "judge_pick_0") - assert game.phase == "round_end" + winner = game.get_player_by_id(winner_id) + assert winner.score == 1 # type: ignore[union-attr] + assert game.phase in ("round_end", "finished") def test_winner_announcement_broadcast(): game, users = _get_to_judging() for u in users: u.clear_messages() - judge = game._get_judges()[0] - game.execute_action(judge, "judge_pick_0") + game.execute_action(game._get_judges()[0], "judge_pick_0") all_spoken = [m for u in users for m in u.get_spoken_messages()] assert any("gets" in m.lower() and "point" in m.lower() for m in all_spoken) @@ -423,23 +310,22 @@ def test_winner_announcement_broadcast(): def test_non_judge_cannot_pick(): game, _ = _get_to_judging() non_judge = game._get_non_judges()[0] - # Non-judges have no valid submissions to pick at this point, action is hidden - submissions_before = list(game.submissions) game.execute_action(non_judge, "judge_pick_0") - # State unchanged - assert game.submissions == submissions_before assert game.phase == "judging" -def test_submissions_shuffled_before_judging(): - # Run many times; at least some ordering should differ from insertion order - game, _ = _setup_game(num_players=4) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - # submission_order exists and covers all submissions - assert len(game.submission_order) == len(game.submissions) - assert sorted(game.submission_order) == list(range(len(game.submissions))) +def test_no_losing_submissions_heading_when_all_are_winners(): + game, users = _setup_game(num_players=3) + game.submissions = [ + {"player_id": "p1", "cards": ["foo"]}, + {"player_id": "p2", "cards": ["bar"]}, + ] + game.current_black_card = _make_black("_ question", pick=1) + for u in users: + u.clear_messages() + game._announce_losing_submissions({"p1", "p2"}) + all_spoken = [m for u in users for m in u.get_spoken_messages()] + assert not any("other submissions" in m.lower() for m in all_spoken) # ========================================================================== @@ -449,173 +335,81 @@ def test_submissions_shuffled_before_judging(): def test_game_ends_when_winning_score_reached(): game, _ = _setup_game(options=HumanityCardsOptions(winning_score=1)) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - judge = game._get_judges()[0] - game.execute_action(judge, "judge_pick_0") + for p in game._get_non_judges(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + game.execute_action(game._get_judges()[0], "judge_pick_0") assert game.status == "finished" def test_round_continues_when_score_below_winning(): game, _ = _setup_game(options=HumanityCardsOptions(winning_score=5)) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - judge = game._get_judges()[0] - game.execute_action(judge, "judge_pick_0") + for p in game._get_non_judges(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + game.execute_action(game._get_judges()[0], "judge_pick_0") assert game.status == "playing" assert game.phase == "round_end" -def test_game_winner_broadcast(): - game, users = _setup_game(options=HumanityCardsOptions(winning_score=1)) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - for u in users: - u.clear_messages() - judge = game._get_judges()[0] - game.execute_action(judge, "judge_pick_0") - all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert any("gets" in m.lower() and "point" in m.lower() for m in all_spoken) - - # ========================================================================== -# Score display (fix 3: speak_l not raw speak) +# Score display # ========================================================================== -def test_check_scores_speaks_all_players_v2(): - game, users = _setup_game(num_players=3) - player0 = _get_player(game, 0) - player0.score = 3 # type: ignore[union-attr] - user0 = next(u for u in users if u.username == player0.name) - user0.clear_messages() - game.execute_action(player0, "check_scores") - spoken = user0.get_spoken_messages() - assert any(player0.name in m for m in spoken) - - -def test_check_scores_includes_score_values(): +def test_check_scores_shows_all_players_and_values(): game, users = _setup_game(num_players=3) - player0 = _get_player(game, 0) + player0 = game.get_active_players()[0] player0.score = 5 # type: ignore[union-attr] game._team_manager.add_to_team_score(player0.name, 5) user0 = next(u for u in users if u.username == player0.name) user0.clear_messages() game.execute_action(player0, "check_scores") - spoken = user0.get_spoken_messages() - assert any("5" in m for m in spoken) - - -def test_check_scores_ordered_descending(): - game, users = _setup_game(num_players=3) - active = game.get_active_players() - active[0].score = 5 # type: ignore[union-attr] - active[1].score = 3 # type: ignore[union-attr] - active[2].score = 1 # type: ignore[union-attr] - # Sync scores into team_manager so check_scores reflects them - for p in active: - game._team_manager.add_to_team_score(p.name, p.score) # type: ignore[union-attr] - user0 = next(u for u in users if u.username == active[0].name) - user0.clear_messages() - game.execute_action(active[0], "check_scores") - spoken = user0.get_spoken_messages() - all_text = " ".join(spoken) - # Highest scorer's name should appear, and their score should be present - assert active[0].name in all_text + all_text = " ".join(user0.get_spoken_messages()) assert "5" in all_text - - -def test_check_scores_speaks_all_players(): - game, users = _setup_game(num_players=3) - player0 = _get_player(game, 0) - user0 = next(u for u in users if u.username == player0.name) - user0.clear_messages() - game.execute_action(player0, "check_scores") - spoken = user0.get_spoken_messages() - # Base class may combine into one message or speak per team — all player names must appear - all_text = " ".join(spoken) for p in game.get_active_players(): assert p.name in all_text # ========================================================================== -# Judge personal announcement (fix 4: hc-you-are-judge) +# Round transition # ========================================================================== -def test_judge_hears_you_are_judge_message(): - game, users = _setup_game(num_players=3) - judge = game._get_judges()[0] - judge_user = next(u for u in users if u.username == judge.name) - spoken = judge_user.get_spoken_messages() - assert any("Card Czar" in m for m in spoken) - - -def test_non_judge_does_not_hear_you_are_judge(): - game, users = _setup_game(num_players=3) - judge_ids = {j.id for j in game._get_judges()} - non_judge_users = [ - u for u in users - if game.get_player_by_id( - next((p.id for p in game.get_active_players() if p.name == u.username), "") - ) is not None - and next( - (p for p in game.get_active_players() if p.name == u.username), None - ) is not None - and next(p for p in game.get_active_players() if p.name == u.username).id not in judge_ids - ] - for u in non_judge_users: - spoken = u.get_spoken_messages() - # Non-judges hear "X is the Card Czar" (broadcast) but NOT "You are the Card Czar" - assert not any(m.startswith("You are the Card Czar") for m in spoken) +def test_round_end_ticks_advance_to_next_round(): + game, _ = _setup_game() + for p in game._get_non_judges(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + game.execute_action(game._get_judges()[0], "judge_pick_0") + assert game.phase == "round_end" + game.round_end_ticks = 1 + game.on_tick() + assert game.phase == "submitting" + assert game.round == 2 -def test_judge_announcement_fires_each_new_round(): +def test_judge_announcement_fires_each_round(): game, users = _setup_game(num_players=3) - # Complete submissions to advance round - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - first_judge = game._get_judges()[0] - old_judge_user = next(u for u in users if u.username == first_judge.name) - old_judge_user.clear_messages() - # Pick winner → round_end → next round - game.execute_action(first_judge, "judge_pick_0") - # Advance tick countdown to trigger next round + for p in game._get_non_judges(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") + game.execute_action(game._get_judges()[0], "judge_pick_0") game.round_end_ticks = 1 game.on_tick() - # New judge should have received announcement new_judge = game._get_judges()[0] new_judge_user = next(u for u in users if u.username == new_judge.name) - spoken = new_judge_user.get_spoken_messages() - assert any("Card Czar" in m for m in spoken) + assert any("Card Czar" in m for m in new_judge_user.get_spoken_messages()) -# ========================================================================== # ========================================================================== # Multi-judge voting # ========================================================================== -def _setup_multi_judge(num_judges: int = 2, num_players: int = 4, judging_method: str = "Independent"): - game, users = _setup_game( - num_players=num_players, - options=HumanityCardsOptions(num_judges=num_judges, judging_method=judging_method), - ) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - assert game.phase == "judging" - return game, users - - def test_multi_judge_waits_for_all_judges(): game, _ = _setup_multi_judge(num_judges=2, num_players=4) judges = game._get_judges() - assert len(judges) == 2 game.execute_action(judges[0], "judge_pick_0") assert game.phase == "judging" assert len(game.judge_picks) == 1 @@ -638,13 +432,11 @@ def test_multi_judge_cannot_vote_twice(): assert game.judge_picks == first_picks -def test_multi_judge_voted_plays_sound(): +def test_multi_judge_intermediate_vote_plays_sound_not_speech(): game, users = _setup_multi_judge(num_judges=2, num_players=4) for u in users: u.clear_messages() - judges = game._get_judges() - game.execute_action(judges[0], "judge_pick_0") - # No speech broadcast for intermediate vote — sound only + game.execute_action(game._get_judges()[0], "judge_pick_0") all_spoken = [m for u in users for m in u.get_spoken_messages()] assert not any("made their choice" in m for m in all_spoken) all_sounds = [s for u in users for s in u.get_sounds_played()] @@ -656,47 +448,32 @@ def test_multi_judge_voted_plays_sound(): # ========================================================================== -def test_independent_single_judge_enforced(): +def test_single_judge_always_uses_independent(): game, _ = _setup_game(options=HumanityCardsOptions(num_judges=1, judging_method="Jury")) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") + for p in game._get_non_judges(): + game.execute_action(p, "toggle_card_0") + game.execute_action(p, "submit_cards") assert game.active_judging_method == "Independent" def test_independent_awards_one_point_per_vote(): game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Independent") judges = game._get_judges() - sub0_player_id = game.submissions[game.submission_order[0]]["player_id"] + sub0_id = game.submissions[game.submission_order[0]]["player_id"] game.execute_action(judges[0], "judge_pick_0") game.execute_action(judges[1], "judge_pick_0") - winner = game.get_player_by_id(sub0_player_id) - assert winner.score == 2 # type: ignore[union-attr] + assert game.get_player_by_id(sub0_id).score == 2 # type: ignore[union-attr] def test_independent_split_vote_both_score(): game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Independent") judges = game._get_judges() - assert len(game.submissions) >= 2 sub0_id = game.submissions[game.submission_order[0]]["player_id"] sub1_id = game.submissions[game.submission_order[1]]["player_id"] game.execute_action(judges[0], "judge_pick_0") game.execute_action(judges[1], "judge_pick_1") - p0 = game.get_player_by_id(sub0_id) - p1 = game.get_player_by_id(sub1_id) - assert p0.score == 1 # type: ignore[union-attr] - assert p1.score == 1 # type: ignore[union-attr] - - -def test_independent_announcement_includes_points(): - game, users = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Independent") - for u in users: - u.clear_messages() - judges = game._get_judges() - game.execute_action(judges[0], "judge_pick_0") - game.execute_action(judges[1], "judge_pick_0") - all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert any("2 points" in m for m in all_spoken) + assert game.get_player_by_id(sub0_id).score == 1 # type: ignore[union-attr] + assert game.get_player_by_id(sub1_id).score == 1 # type: ignore[union-attr] def test_jury_sole_winner_gets_one_point(): @@ -705,34 +482,18 @@ def test_jury_sole_winner_gets_one_point(): sub0_id = game.submissions[game.submission_order[0]]["player_id"] game.execute_action(judges[0], "judge_pick_0") game.execute_action(judges[1], "judge_pick_0") - winner = game.get_player_by_id(sub0_id) - assert winner.score == 1 # type: ignore[union-attr] + assert game.get_player_by_id(sub0_id).score == 1 # type: ignore[union-attr] -def test_jury_tie_both_score(): +def test_jury_tie_both_score_one_point(): game, _ = _setup_multi_judge(num_judges=2, num_players=4, judging_method="Jury") judges = game._get_judges() - assert len(game.submissions) >= 2 sub0_id = game.submissions[game.submission_order[0]]["player_id"] sub1_id = game.submissions[game.submission_order[1]]["player_id"] game.execute_action(judges[0], "judge_pick_0") game.execute_action(judges[1], "judge_pick_1") - p0 = game.get_player_by_id(sub0_id) - p1 = game.get_player_by_id(sub1_id) - assert p0.score == 1 # type: ignore[union-attr] - assert p1.score == 1 # type: ignore[union-attr] - - -def test_jury_single_judge_uses_independent_enforcement(): - # With 1 judge, active_judging_method = Independent regardless of setting - game, _ = _setup_game( - num_players=3, - options=HumanityCardsOptions(num_judges=1, judging_method="Jury"), - ) - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - assert game.active_judging_method == "Independent" + assert game.get_player_by_id(sub0_id).score == 1 # type: ignore[union-attr] + assert game.get_player_by_id(sub1_id).score == 1 # type: ignore[union-attr] def test_random_method_resolves_to_independent_or_jury(): @@ -740,40 +501,8 @@ def test_random_method_resolves_to_independent_or_jury(): assert game.active_judging_method in ("Independent", "Jury") -def test_wrong_card_count_speaks_error_not_raw_tuple(): - game, users = _setup_game(num_players=3) - game.current_black_card = _make_black("_ with _ always.", pick=2) - non_judge = game._get_non_judges()[0] - non_judge_user = next(u for u in users if u.username == non_judge.name) - # Select only 1 card, need 2 - game.execute_action(non_judge, "toggle_card_0") - non_judge_user.clear_messages() - game.execute_action(non_judge, "submit_cards") - spoken = non_judge_user.get_spoken_messages() - assert spoken, "should have spoken an error" - assert not any("hc-wrong-card-count" in m for m in spoken), "raw key leaked into speech" - assert any("2" in m for m in spoken) - - -# Round transition via ticks # ========================================================================== - - -def test_round_end_ticks_advance_to_next_round(): - game, _ = _setup_game() - for non_judge in game._get_non_judges(): - game.execute_action(non_judge, "toggle_card_0") - game.execute_action(non_judge, "submit_cards") - game.execute_action(game._get_judges()[0], "judge_pick_0") - assert game.phase == "round_end" - game.round_end_ticks = 1 - game.on_tick() - assert game.phase == "submitting" - assert game.round == 2 - - -# ========================================================================== -# Full bot game +# Bot game # ========================================================================== @@ -784,64 +513,21 @@ def test_bot_game_completes(): for i in range(4): game.add_player(f"Bot{i}", Bot(f"Bot{i}")) game.on_start() - for _ in range(100_000): if game.status == "finished": break game.on_tick() - assert game.status == "finished" -def test_bot_game_all_players_score_tracked(): - opts = HumanityCardsOptions(winning_score=2) - game = HumanityCardsGame(options=opts) - game._build_decks = lambda: _inject_decks(game, white_count=500, black_count=100) # type: ignore[method-assign] - for i in range(3): - game.add_player(f"Bot{i}", Bot(f"Bot{i}")) - game.on_start() - for _ in range(100_000): - if game.status == "finished": - break - game.on_tick() - total_score = sum(p.score for p in game.get_active_players()) # type: ignore[union-attr] - assert total_score >= opts.winning_score - - # ========================================================================== # All-judge mode # ========================================================================== -def _setup_all_judge(num_players: int = 3) -> tuple[HumanityCardsGame, list]: - """Set up game in submitting phase with everyone as judge.""" - opts = HumanityCardsOptions(num_judges=num_players) - game, users = _setup_game(num_players=num_players, options=opts) - return game, users - - -def test_all_judge_mode_everyone_is_judge(): - game, _ = _setup_all_judge(num_players=3) +def test_all_judge_mode_everyone_submits_and_judges(): + game, _ = _setup_game(num_players=3, options=HumanityCardsOptions(num_judges=3)) assert game._all_players_are_judges() - assert len(game._get_judges()) == 3 - - -def test_all_judge_mode_submitters_are_all_players(): - game, _ = _setup_all_judge(num_players=3) - submitters = game._get_submitters() - active = game.get_active_players() - assert len(submitters) == len(active) - - -def test_all_judge_mode_players_can_submit(): - game, users = _setup_all_judge(num_players=3) - for p in game.get_active_players(): - assert game._is_toggle_card_enabled(p, "toggle_card_0") is None - assert game._is_submit_enabled(p) is None - - -def test_all_judge_mode_submitting_progresses_to_judging(): - game, _ = _setup_all_judge(num_players=3) for p in game.get_active_players(): game.execute_action(p, "toggle_card_0") game.execute_action(p, "submit_cards") @@ -850,51 +536,26 @@ def test_all_judge_mode_submitting_progresses_to_judging(): def test_all_judge_mode_self_vote_hidden(): - game, _ = _setup_all_judge(num_players=3) - for p in game.get_active_players(): - game.execute_action(p, "toggle_card_0") - game.execute_action(p, "submit_cards") - assert game.phase == "judging" - judges = game._get_judges() - for judge in judges: - # Find which pick index is their own submission + game, _ = _get_to_judging(options=HumanityCardsOptions(num_judges=3)) + for judge in game._get_judges(): for i, sub_idx in enumerate(game.submission_order): - sub = game.submissions[sub_idx] - if sub["player_id"] == judge.id: - vis = game._is_judge_pick_hidden(judge, f"judge_pick_{i}") - assert vis == Visibility.HIDDEN, f"{judge.name} can see own submission at pick_{i}" + if game.submissions[sub_idx]["player_id"] == judge.id: + assert game._is_judge_pick_hidden(judge, f"judge_pick_{i}") == Visibility.HIDDEN -def test_all_judge_mode_self_vote_blocked_in_action(): - game, _ = _setup_all_judge(num_players=3) - for p in game.get_active_players(): - game.execute_action(p, "toggle_card_0") - game.execute_action(p, "submit_cards") - assert game.phase == "judging" - judges = game._get_judges() - for judge in judges: +def test_all_judge_mode_self_vote_blocked(): + game, _ = _get_to_judging(options=HumanityCardsOptions(num_judges=3)) + for judge in game._get_judges(): for i, sub_idx in enumerate(game.submission_order): - sub = game.submissions[sub_idx] - if sub["player_id"] == judge.id: + if game.submissions[sub_idx]["player_id"] == judge.id: game._judge_pick(judge, i) - assert judge.id not in game.judge_picks, f"{judge.name} voted for self" + assert judge.id not in game.judge_picks -def test_no_losing_submissions_heading_when_all_are_winners(): - """_announce_losing_submissions should not broadcast the heading if there are no losers.""" - game, users = _setup_game(num_players=3) - # Inject fake submissions - game.submissions = [ - {"player_id": "p1", "cards": ["foo"]}, - {"player_id": "p2", "cards": ["bar"]}, - ] - game.current_black_card = _make_black("_ question", pick=1) - for u in users: - u.clear_messages() - # All submission player_ids are winners — no losers - game._announce_losing_submissions({"p1", "p2"}) +def test_all_judge_mode_no_czar_announcement(): + game, users = _setup_game(num_players=3, options=HumanityCardsOptions(num_judges=3)) all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert not any("other submissions" in m.lower() for m in all_spoken) + assert not any("Card Czar" in m for m in all_spoken) def test_all_judge_bot_game_completes(): @@ -912,7 +573,7 @@ def test_all_judge_bot_game_completes(): # ========================================================================== -# Grammar and announcement fixes +# Judge announcement grammar # ========================================================================== @@ -920,12 +581,10 @@ def test_judge_announcement_two_judges_grammar(): game, users = _setup_game(num_players=4, options=HumanityCardsOptions(num_judges=2)) all_spoken = [m for u in users for m in u.get_spoken_messages()] judge_names = [j.name for j in game._get_judges()] - # Should be "Alice and Bob are the Card Czars" — no trailing "and X, Y" - judge_announce = [m for m in all_spoken if "Card Czar" in m] - assert judge_announce, "no judge announcement found" - msg = judge_announce[0] - assert judge_names[0] in msg - assert judge_names[1] in msg + msgs = [m for m in all_spoken if "Card Czar" in m] + assert msgs + msg = msgs[0] + assert all(n in msg for n in judge_names) assert msg.count("and") == 1 @@ -933,33 +592,20 @@ def test_judge_announcement_three_judges_oxford_comma(): game, users = _setup_game(num_players=4, options=HumanityCardsOptions(num_judges=3)) all_spoken = [m for u in users for m in u.get_spoken_messages()] judge_names = [j.name for j in game._get_judges()] - judge_announce = [m for m in all_spoken if "Card Czar" in m] - assert judge_announce - msg = judge_announce[0] - # All names present, with Oxford comma pattern - for name in judge_names: - assert name in msg - assert ", and " in msg - - -def test_all_judge_mode_no_czar_announcement(): - game, users = _setup_all_judge(num_players=3) - all_spoken = [m for u in users for m in u.get_spoken_messages()] - assert not any("Card Czar" in m for m in all_spoken) + msgs = [m for m in all_spoken if "Card Czar" in m] + assert msgs + assert all(n in msgs[0] for n in judge_names) + assert ", and " in msgs[0] def test_whose_turn_judging_lists_pending_judges(): game, users = _setup_multi_judge(num_judges=2, num_players=4) judges = game._get_judges() - # First judge votes game.execute_action(judges[0], "judge_pick_0") - assert game.phase == "judging" - # Use a non-judge player to call whose_turn non_judge = game._get_non_judges()[0] non_judge_user = next(u for u in users if u.username == non_judge.name) non_judge_user.clear_messages() game._action_whose_turn(non_judge, "whose_turn") spoken = non_judge_user.get_spoken_messages() - # Should mention the pending judge, not "submitted" assert any(judges[1].name in m for m in spoken) assert not any("submitted" in m.lower() for m in spoken) From 0ab397105a45c1b40d7675d3e85be154ac7b02c2 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Tue, 21 Apr 2026 22:56:35 -0400 Subject: [PATCH 14/14] Fixed Cards Against Humanity missing localization strings --- server/games/humanitycards/game.py | 42 ++++++++++++++++++++--------- server/locales/ar/humanitycards.ftl | 18 ++++++++----- server/locales/cs/humanitycards.ftl | 18 ++++++++----- server/locales/de/humanitycards.ftl | 18 ++++++++----- server/locales/es/humanitycards.ftl | 18 ++++++++----- server/locales/fa/humanitycards.ftl | 18 ++++++++----- server/locales/fr/humanitycards.ftl | 18 ++++++++----- server/locales/hi/humanitycards.ftl | 18 ++++++++----- server/locales/hr/humanitycards.ftl | 18 ++++++++----- server/locales/hu/humanitycards.ftl | 18 ++++++++----- server/locales/id/humanitycards.ftl | 18 ++++++++----- server/locales/it/humanitycards.ftl | 18 ++++++++----- server/locales/ja/humanitycards.ftl | 18 ++++++++----- server/locales/ko/humanitycards.ftl | 18 ++++++++----- server/locales/mn/humanitycards.ftl | 18 ++++++++----- server/locales/nl/humanitycards.ftl | 18 ++++++++----- server/locales/pl/humanitycards.ftl | 18 ++++++++----- server/locales/pt/humanitycards.ftl | 18 ++++++++----- server/locales/ro/humanitycards.ftl | 18 ++++++++----- server/locales/ru/humanitycards.ftl | 18 ++++++++----- server/locales/sk/humanitycards.ftl | 18 ++++++++----- server/locales/sl/humanitycards.ftl | 18 ++++++++----- server/locales/sr/humanitycards.ftl | 18 ++++++++----- server/locales/sv/humanitycards.ftl | 18 ++++++++----- server/locales/th/humanitycards.ftl | 18 ++++++++----- server/locales/tr/humanitycards.ftl | 18 ++++++++----- server/locales/uk/humanitycards.ftl | 18 ++++++++----- server/locales/vi/humanitycards.ftl | 18 ++++++++----- server/locales/zh/humanitycards.ftl | 18 ++++++++----- server/locales/zu/humanitycards.ftl | 18 ++++++++----- 30 files changed, 348 insertions(+), 216 deletions(-) diff --git a/server/games/humanitycards/game.py b/server/games/humanitycards/game.py index 1845baa7..e1a9baef 100644 --- a/server/games/humanitycards/game.py +++ b/server/games/humanitycards/game.py @@ -409,13 +409,9 @@ def _all_players_are_judges(self) -> bool: return len(self._get_judges()) >= len(self.get_active_players()) @staticmethod - def _format_names(names: list[str]) -> str: - """Format a list of names as a grammatically correct series.""" - if len(names) == 1: - return names[0] - if len(names) == 2: - return f"{names[0]} and {names[1]}" - return ", ".join(names[:-1]) + f", and {names[-1]}" + def _format_names(locale: str, names: list[str]) -> str: + """Format a list of names using the recipient's locale rules.""" + return Localization.format_list_and(locale, names) def _get_submitters(self) -> list[HumanityCardsPlayer]: """Players expected to submit this round (everyone when all are judges).""" @@ -816,7 +812,7 @@ def _action_whose_judge(self, player: Player, action_id: str) -> None: return judges = self._get_judges() if judges: - names = self._format_names([j.name for j in judges]) + names = self._format_names(user.locale, [j.name for j in judges]) user.speak_l("hc-judge-is", names=names, count=len(judges)) def _action_whose_turn(self, player: Player, action_id: str) -> None: @@ -828,17 +824,20 @@ def _action_whose_turn(self, player: Player, action_id: str) -> None: if self.phase == "submitting": waiting = [p.name for p in self._get_submitters() if p.submitted_cards is None] if waiting: - user.speak_l("hc-waiting-for", names=self._format_names(waiting)) + user.speak_l("hc-waiting-for", names=self._format_names(user.locale, waiting)) else: judges = self._get_judges() user.speak_l( "hc-all-submitted-waiting-judge", - judge=self._format_names([j.name for j in judges]), + judge=self._format_names(user.locale, [j.name for j in judges]), ) elif self.phase == "judging": pending = [j for j in self._get_judges() if j.id not in self.judge_picks] if pending: - user.speak_l("hc-waiting-for-judges", names=self._format_names([j.name for j in pending])) + user.speak_l( + "hc-waiting-for-judges", + names=self._format_names(user.locale, [j.name for j in pending]), + ) else: user.speak_l("game-no-turn") else: @@ -1319,8 +1318,25 @@ def _start_round(self) -> None: # Announce judge(s) — skip in all-judge mode (everyone knows) judges = self._get_judges() if not self._all_players_are_judges(): - names = self._format_names([j.name for j in judges]) - self.broadcast_l("hc-judge-is", names=names, count=len(judges)) + judge_names = [j.name for j in judges] + for player in self.players: + user = self.get_user(player) + locale = user.locale if user else "en" + localized = Localization.get( + locale, + "hc-judge-is", + names=self._format_names(locale, judge_names), + count=len(judges), + ) + if hasattr(self, "record_transcript_event"): + self.record_transcript_event(player, localized, "table") + if user: + user.speak_l( + "hc-judge-is", + "table", + names=self._format_names(locale, judge_names), + count=len(judges), + ) for judge in judges: user = self.get_user(judge) if user: diff --git a/server/locales/ar/humanitycards.ftl b/server/locales/ar/humanitycards.ftl index 56a75520..3d464068 100644 --- a/server/locales/ar/humanitycards.ftl +++ b/server/locales/ar/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } هو قيصر البطاقات. + *[other] { $names } هم قياصرة البطاقات. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = يحصل { $player } على { $points } { $points -> + [one] نقطة + *[other] نقاط +} عن { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = بانتظار { $names } لتقديم الإجابة. +hc-all-submitted-waiting-judge = قدّم جميع اللاعبين إجاباتهم. بانتظار { $judge } للحكم. +hc-waiting-for-judges = بانتظار { $names } ليحكموا. diff --git a/server/locales/cs/humanitycards.ftl b/server/locales/cs/humanitycards.ftl index 56a75520..080be5b3 100644 --- a/server/locales/cs/humanitycards.ftl +++ b/server/locales/cs/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } je karetní car. + *[other] { $names } jsou karetní caři. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } získává { $points } { $points -> + [one] bod + *[other] body +} za { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Čeká se na { $names }, až odevzdají karty. +hc-all-submitted-waiting-judge = Všichni hráči odevzdali karty. Čeká se na rozhodnutí od { $judge }. +hc-waiting-for-judges = Čeká se na rozhodnutí od { $names }. diff --git a/server/locales/de/humanitycards.ftl b/server/locales/de/humanitycards.ftl index 56a75520..b2352b0e 100644 --- a/server/locales/de/humanitycards.ftl +++ b/server/locales/de/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } ist der Kartenzar. + *[other] { $names } sind die Kartenzare. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } erhält { $points } { $points -> + [one] Punkt + *[other] Punkte +} für { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Es wird auf { $names } gewartet, bis die Karten eingereicht werden. +hc-all-submitted-waiting-judge = Alle Spieler haben ihre Karten eingereicht. Es wird auf die Entscheidung von { $judge } gewartet. +hc-waiting-for-judges = Es wird auf die Entscheidung von { $names } gewartet. diff --git a/server/locales/es/humanitycards.ftl b/server/locales/es/humanitycards.ftl index 56a75520..64afb587 100644 --- a/server/locales/es/humanitycards.ftl +++ b/server/locales/es/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } es el zar de las cartas. + *[other] { $names } son los zares de las cartas. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } obtiene { $points } { $points -> + [one] punto + *[other] puntos +} por { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Esperando a { $names } para enviar sus cartas. +hc-all-submitted-waiting-judge = Todos los jugadores han enviado sus cartas. Esperando a { $judge } para juzgar. +hc-waiting-for-judges = Esperando a { $names } para juzgar. diff --git a/server/locales/fa/humanitycards.ftl b/server/locales/fa/humanitycards.ftl index 56a75520..84e822dd 100644 --- a/server/locales/fa/humanitycards.ftl +++ b/server/locales/fa/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } تزار کارت است. + *[other] { $names } تزارهای کارت هستند. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } برای { $text }، { $points } { $points -> + [one] امتیاز + *[other] امتیاز +} می‌گیرد. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = در انتظار { $names } برای ارسال پاسخ. +hc-all-submitted-waiting-judge = همه بازیکنان پاسخ خود را فرستاده‌اند. در انتظار داوری { $judge }. +hc-waiting-for-judges = در انتظار { $names } برای داوری. diff --git a/server/locales/fr/humanitycards.ftl b/server/locales/fr/humanitycards.ftl index 56a75520..46d17f98 100644 --- a/server/locales/fr/humanitycards.ftl +++ b/server/locales/fr/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } est le tsar des cartes. + *[other] { $names } sont les tsars des cartes. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } obtient { $points } { $points -> + [one] point + *[other] points +} pour { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = En attente de la soumission de { $names }. +hc-all-submitted-waiting-judge = Tous les joueurs ont soumis leurs cartes. En attente du jugement de { $judge }. +hc-waiting-for-judges = En attente du jugement de { $names }. diff --git a/server/locales/hi/humanitycards.ftl b/server/locales/hi/humanitycards.ftl index 56a75520..3803d409 100644 --- a/server/locales/hi/humanitycards.ftl +++ b/server/locales/hi/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } कार्ड ज़ार है। + *[other] { $names } कार्ड ज़ार हैं। +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } को { $text } के लिए { $points } { $points -> + [one] अंक + *[other] अंक +} मिलते हैं। hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = { $names } के जमा करने का इंतज़ार है। +hc-all-submitted-waiting-judge = सभी खिलाड़ियों ने अपने जवाब जमा कर दिए हैं। { $judge } के निर्णय का इंतज़ार है। +hc-waiting-for-judges = { $names } के निर्णय का इंतज़ार है। diff --git a/server/locales/hr/humanitycards.ftl b/server/locales/hr/humanitycards.ftl index 56a75520..a6a3f400 100644 --- a/server/locales/hr/humanitycards.ftl +++ b/server/locales/hr/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } je car karata. + *[other] { $names } su carevi karata. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } dobiva { $points } { $points -> + [one] bod + *[other] bodova +} za { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Čeka se da { $names } predaju karte. +hc-all-submitted-waiting-judge = Svi igrači su predali karte. Čeka se da { $judge } presudi. +hc-waiting-for-judges = Čeka se da { $names } presude. diff --git a/server/locales/hu/humanitycards.ftl b/server/locales/hu/humanitycards.ftl index 56a75520..c5aba35d 100644 --- a/server/locales/hu/humanitycards.ftl +++ b/server/locales/hu/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } a kártyacár. + *[other] { $names } a kártyacárok. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } { $points } { $points -> + [one] pontot + *[other] pontot +} kap ezért: { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Várakozás erre: { $names }, hogy beküldjék a lapjaikat. +hc-all-submitted-waiting-judge = Minden játékos beküldte a lapjait. Várakozás erre: { $judge }, hogy bíráljon. +hc-waiting-for-judges = Várakozás erre: { $names }, hogy bíráljanak. diff --git a/server/locales/id/humanitycards.ftl b/server/locales/id/humanitycards.ftl index 56a75520..9c82110c 100644 --- a/server/locales/id/humanitycards.ftl +++ b/server/locales/id/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } adalah Card Czar. + *[other] { $names } adalah para Card Czar. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } mendapat { $points } { $points -> + [one] poin + *[other] poin +} untuk { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Menunggu { $names } mengirimkan kartu. +hc-all-submitted-waiting-judge = Semua pemain sudah mengirimkan kartu. Menunggu { $judge } untuk menilai. +hc-waiting-for-judges = Menunggu { $names } untuk menilai. diff --git a/server/locales/it/humanitycards.ftl b/server/locales/it/humanitycards.ftl index 56a75520..c886b053 100644 --- a/server/locales/it/humanitycards.ftl +++ b/server/locales/it/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } è lo zar delle carte. + *[other] { $names } sono gli zar delle carte. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } ottiene { $points } { $points -> + [one] punto + *[other] punti +} per { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = In attesa che { $names } inviino le carte. +hc-all-submitted-waiting-judge = Tutti i giocatori hanno inviato le carte. In attesa del giudizio di { $judge }. +hc-waiting-for-judges = In attesa del giudizio di { $names }. diff --git a/server/locales/ja/humanitycards.ftl b/server/locales/ja/humanitycards.ftl index 56a75520..df026bae 100644 --- a/server/locales/ja/humanitycards.ftl +++ b/server/locales/ja/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } がカードツァーです。 + *[other] { $names } がカードツァーです。 +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } は { $text } で { $points } { $points -> + [one] 点 + *[other] 点 +} を獲得します。 hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = { $names } の提出を待っています。 +hc-all-submitted-waiting-judge = 全員が提出しました。{ $judge } の判定を待っています。 +hc-waiting-for-judges = { $names } の判定を待っています。 diff --git a/server/locales/ko/humanitycards.ftl b/server/locales/ko/humanitycards.ftl index 56a75520..61a6e74a 100644 --- a/server/locales/ko/humanitycards.ftl +++ b/server/locales/ko/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } 님이 카드 차르입니다. + *[other] { $names } 님이 카드 차르들입니다. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } 님이 { $text }(으)로 { $points } { $points -> + [one]점 + *[other]점 +}을 얻었습니다. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = { $names } 님의 제출을 기다리는 중입니다. +hc-all-submitted-waiting-judge = 모든 플레이어가 제출했습니다. { $judge } 님의 심사를 기다리는 중입니다. +hc-waiting-for-judges = { $names } 님의 심사를 기다리는 중입니다. diff --git a/server/locales/mn/humanitycards.ftl b/server/locales/mn/humanitycards.ftl index 56a75520..8aaa7754 100644 --- a/server/locales/mn/humanitycards.ftl +++ b/server/locales/mn/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } бол картын хаан. + *[other] { $names } бол картын хаад. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } { $text }-д { $points } { $points -> + [one] оноо + *[other] оноо +} авлаа. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = { $names } илгээхийг хүлээж байна. +hc-all-submitted-waiting-judge = Бүх тоглогч хариугаа илгээсэн. { $judge } шүүхийг хүлээж байна. +hc-waiting-for-judges = { $names } шүүхийг хүлээж байна. diff --git a/server/locales/nl/humanitycards.ftl b/server/locales/nl/humanitycards.ftl index 56a75520..dbab0462 100644 --- a/server/locales/nl/humanitycards.ftl +++ b/server/locales/nl/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } is de kaarttsaar. + *[other] { $names } zijn de kaarttsaren. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } krijgt { $points } { $points -> + [one] punt + *[other] punten +} voor { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Wachten op { $names } om in te dienen. +hc-all-submitted-waiting-judge = Alle spelers hebben ingezonden. Wachten op het oordeel van { $judge }. +hc-waiting-for-judges = Wachten op het oordeel van { $names }. diff --git a/server/locales/pl/humanitycards.ftl b/server/locales/pl/humanitycards.ftl index 56a75520..1bc9bbf7 100644 --- a/server/locales/pl/humanitycards.ftl +++ b/server/locales/pl/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } jest carem kart. + *[other] { $names } są carami kart. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } zdobywa { $points } { $points -> + [one] punkt + *[other] punkty +} za { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Oczekiwanie na ruch od { $names }. +hc-all-submitted-waiting-judge = Wszyscy gracze już zagrali. Oczekiwanie na ocenę od { $judge }. +hc-waiting-for-judges = Oczekiwanie na ocenę od { $names }. diff --git a/server/locales/pt/humanitycards.ftl b/server/locales/pt/humanitycards.ftl index 56a75520..e2c4f4b4 100644 --- a/server/locales/pt/humanitycards.ftl +++ b/server/locales/pt/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } é o czar das cartas. + *[other] { $names } são os czares das cartas. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } recebe { $points } { $points -> + [one] ponto + *[other] pontos +} por { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Aguardando { $names } enviarem as cartas. +hc-all-submitted-waiting-judge = Todos os jogadores enviaram suas cartas. Aguardando o julgamento de { $judge }. +hc-waiting-for-judges = Aguardando o julgamento de { $names }. diff --git a/server/locales/ro/humanitycards.ftl b/server/locales/ro/humanitycards.ftl index 56a75520..c6445fce 100644 --- a/server/locales/ro/humanitycards.ftl +++ b/server/locales/ro/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } este țarul cărților. + *[other] { $names } sunt țarii cărților. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } primește { $points } { $points -> + [one] punct + *[other] puncte +} pentru { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Se așteaptă ca { $names } să trimită cărțile. +hc-all-submitted-waiting-judge = Toți jucătorii au trimis cărțile. Se așteaptă verdictul lui { $judge }. +hc-waiting-for-judges = Se așteaptă verdictul lui { $names }. diff --git a/server/locales/ru/humanitycards.ftl b/server/locales/ru/humanitycards.ftl index 56a75520..9833db1c 100644 --- a/server/locales/ru/humanitycards.ftl +++ b/server/locales/ru/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } — карточный царь. + *[other] { $names } — карточные цари. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } получает { $points } { $points -> + [one] очко + *[other] очков +} за { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Ожидание, пока { $names } сдадут карты. +hc-all-submitted-waiting-judge = Все игроки уже сдали карты. Ожидание решения от { $judge }. +hc-waiting-for-judges = Ожидание решения от { $names }. diff --git a/server/locales/sk/humanitycards.ftl b/server/locales/sk/humanitycards.ftl index 56a75520..e6b0b707 100644 --- a/server/locales/sk/humanitycards.ftl +++ b/server/locales/sk/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } je kartový cár. + *[other] { $names } sú kartoví cári. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } získava { $points } { $points -> + [one] bod + *[other] body +} za { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Čaká sa na { $names }, kým odovzdajú karty. +hc-all-submitted-waiting-judge = Všetci hráči už odovzdali karty. Čaká sa na rozhodnutie od { $judge }. +hc-waiting-for-judges = Čaká sa na rozhodnutie od { $names }. diff --git a/server/locales/sl/humanitycards.ftl b/server/locales/sl/humanitycards.ftl index 56a75520..f2a38999 100644 --- a/server/locales/sl/humanitycards.ftl +++ b/server/locales/sl/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } je car kart. + *[other] { $names } so carji kart. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } dobi { $points } { $points -> + [one] točko + *[other] točke +} za { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Čaka se na { $names }, da oddajo karte. +hc-all-submitted-waiting-judge = Vsi igralci so oddali karte. Čaka se na odločitev od { $judge }. +hc-waiting-for-judges = Čaka se na odločitev od { $names }. diff --git a/server/locales/sr/humanitycards.ftl b/server/locales/sr/humanitycards.ftl index 420a7bcf..42e29032 100644 --- a/server/locales/sr/humanitycards.ftl +++ b/server/locales/sr/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Deljenje { $count } karata svakom igraču. hc-round-start = Runda { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] je sudija - *[other] i { $others } su sudije -}. +hc-judge-is = { $count -> + [one] { $names } je sudija. + *[other] { $names } su sudije. +} hc-you-are-judge = Vi ste sudija u ovoj rundi. hc-you-are-not-judge = Niste sudija u ovoj rundi. @@ -68,7 +68,10 @@ hc-select-winner-prompt = Izaberite pobednički predlog hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } dobija rundu! Rezultat: { $score }. +hc-winner-announcement = { $player } dobija { $points } { $points -> + [one] poen + *[other] poena +} za { $text }. hc-winner-card = Pobednički odgovor: { $text } hc-round-scores = Rezultat nakon runde { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -103,5 +106,6 @@ hc-no-scores = Još uvek nema rezultata. # Whose turn / whose judge hc-whose-judge = Ko je sudija -hc-waiting-for = Čeka se da { $names } igraju. -hc-all-submitted-waiting-judge = Svi igrači su poslali svoje predloge. Čeka se da { $judge } sudi. +hc-waiting-for = Čeka se da { $names } odigraju. +hc-all-submitted-waiting-judge = Svi igrači su poslali svoje predloge. Čeka se da { $judge } presudi. +hc-waiting-for-judges = Čeka se da { $names } presude. diff --git a/server/locales/sv/humanitycards.ftl b/server/locales/sv/humanitycards.ftl index 56a75520..b8d651ca 100644 --- a/server/locales/sv/humanitycards.ftl +++ b/server/locales/sv/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } är korttsaren. + *[other] { $names } är korttsarerna. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } får { $points } { $points -> + [one] poäng + *[other] poäng +} för { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Väntar på att { $names } ska lämna in. +hc-all-submitted-waiting-judge = Alla spelare har lämnat in. Väntar på att { $judge } ska döma. +hc-waiting-for-judges = Väntar på att { $names } ska döma. diff --git a/server/locales/th/humanitycards.ftl b/server/locales/th/humanitycards.ftl index 56a75520..34c63e48 100644 --- a/server/locales/th/humanitycards.ftl +++ b/server/locales/th/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } คือซาร์ไพ่ + *[other] { $names } คือซาร์ไพ่ +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } ได้ { $points } { $points -> + [one] คะแนน + *[other] คะแนน +} จาก { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = กำลังรอ { $names } ส่งคำตอบ +hc-all-submitted-waiting-judge = ผู้เล่นทุกคนส่งคำตอบแล้ว กำลังรอ { $judge } ตัดสิน +hc-waiting-for-judges = กำลังรอ { $names } ตัดสิน diff --git a/server/locales/tr/humanitycards.ftl b/server/locales/tr/humanitycards.ftl index 56a75520..661a9e8c 100644 --- a/server/locales/tr/humanitycards.ftl +++ b/server/locales/tr/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } kart çarıdır. + *[other] { $names } kart çarlarıdır. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player }, { $text } için { $points } { $points -> + [one] puan + *[other] puan +} alır. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = { $names } gönderene kadar bekleniyor. +hc-all-submitted-waiting-judge = Tüm oyuncular kartlarını gönderdi. { $judge } karar verene kadar bekleniyor. +hc-waiting-for-judges = { $names } karar verene kadar bekleniyor. diff --git a/server/locales/uk/humanitycards.ftl b/server/locales/uk/humanitycards.ftl index 56a75520..e8d111f3 100644 --- a/server/locales/uk/humanitycards.ftl +++ b/server/locales/uk/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } — картковий цар. + *[other] { $names } — карткові царі. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } отримує { $points } { $points -> + [one] очко + *[other] очок +} за { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Очікування, поки { $names } подадуть карти. +hc-all-submitted-waiting-judge = Усі гравці вже подали карти. Очікування рішення від { $judge }. +hc-waiting-for-judges = Очікування рішення від { $names }. diff --git a/server/locales/vi/humanitycards.ftl b/server/locales/vi/humanitycards.ftl index 56a75520..336a01bf 100644 --- a/server/locales/vi/humanitycards.ftl +++ b/server/locales/vi/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } là Sa hoàng thẻ bài. + *[other] { $names } là các Sa hoàng thẻ bài. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } nhận được { $points } { $points -> + [one] điểm + *[other] điểm +} cho { $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Đang chờ { $names } nộp bài. +hc-all-submitted-waiting-judge = Tất cả người chơi đã nộp bài. Đang chờ { $judge } chấm. +hc-waiting-for-judges = Đang chờ { $names } chấm. diff --git a/server/locales/zh/humanitycards.ftl b/server/locales/zh/humanitycards.ftl index 56a75520..756715c2 100644 --- a/server/locales/zh/humanitycards.ftl +++ b/server/locales/zh/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } 是本轮判官。 + *[other] { $names } 是本轮判官。 +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } 凭借 { $text } 获得 { $points } { $points -> + [one] 分 + *[other] 分 +}。 hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = 正在等待 { $names } 提交答案。 +hc-all-submitted-waiting-judge = 所有玩家都已提交答案。正在等待 { $judge } 评判。 +hc-waiting-for-judges = 正在等待 { $names } 评判。 diff --git a/server/locales/zu/humanitycards.ftl b/server/locales/zu/humanitycards.ftl index 56a75520..009af276 100644 --- a/server/locales/zu/humanitycards.ftl +++ b/server/locales/zu/humanitycards.ftl @@ -30,10 +30,10 @@ hc-dealing-cards = Dealing { $count } cards to each player. hc-round-start = Round { $round }. # Judge announcement -hc-judge-is = { $player } { $count -> - [one] is the Card Czar - *[other] and { $others } are the Card Czars -}. +hc-judge-is = { $count -> + [one] { $names } uyinkosi yamakhadi. + *[other] { $names } bayizinkosi zamakhadi. +} hc-you-are-judge = You are the Card Czar this round. hc-you-are-not-judge = You are not the Card Czar this round. @@ -66,7 +66,10 @@ hc-select-winner-prompt = Select the winning submission hc-submission-option = { $text } # Results -hc-winner-announcement = { $player } wins the round! Score: { $score }. +hc-winner-announcement = { $player } uthola { $points } { $points -> + [one] iphuzu + *[other] amaphuzu +} ngo-{ $text }. hc-winner-card = Winning answer: { $text } hc-round-scores = Scores after round { $round }: hc-score-line = { $player }: { $score } { $score -> @@ -101,5 +104,6 @@ hc-no-scores = No scores yet. # Whose turn / whose judge hc-whose-judge = Who is judging -hc-waiting-for = Waiting for { $names } to submit. -hc-all-submitted-waiting-judge = All players have submitted. Waiting for { $judge } to judge. +hc-waiting-for = Kulindwe { $names } ukuthi bathumele. +hc-all-submitted-waiting-judge = Bonke abadlali sebethumele. Kulindwe { $judge } ukuthi ahlulele. +hc-waiting-for-judges = Kulindwe { $names } ukuthi bahlulele.