matiqorb/spades-agent-api
0
1from __future__ import annotations2 3from dataclasses import dataclass4import random5from typing import List6 7from .engine import Card, SpadesGame, team_for_player8 9 10@dataclass11class BaseBot:12 seed: int = 013 14 def __post_init__(self) -> None:15 self.rng = random.Random(self.seed)16 17 def choose_bid(self, game: SpadesGame, player: int) -> int:18 return max(1, min(13, len([c for c in game.hands[player] if c.rank >= 12])))19 20 def choose_card(self, game: SpadesGame, player: int) -> Card:21 legal = sorted(game.legal_cards(player))22 return legal[0]23 24 25class SafeBot(BaseBot):26 def choose_bid(self, game: SpadesGame, player: int) -> int:27 hand = game.hands[player]28 score = 0.029 for card in hand:30 if card.suit == "S":31 score += 0.4532 if card.rank >= 11:33 score += 0.634 elif card.rank >= 13:35 score += 0.536 elif card.rank == 12:37 score += 0.238 return max(1, min(8, int(round(score))))39 40 def choose_card(self, game: SpadesGame, player: int) -> Card:41 legal = sorted(game.legal_cards(player))42 return legal[0]43 44 45class GreedyBot(BaseBot):46 def choose_bid(self, game: SpadesGame, player: int) -> int:47 hand = game.hands[player]48 score = 0.049 for card in hand:50 if card.suit == "S":51 score += 0.952 if card.rank >= 13:53 score += 0.754 elif card.rank == 12:55 score += 0.356 return max(2, min(13, int(round(score))))57 58 def choose_card(self, game: SpadesGame, player: int) -> Card:59 legal = sorted(game.legal_cards(player))60 lead = not bool(game.trick)61 if lead:62 return legal[-1]63 winning = _winning_candidates(game, legal)64 return winning[-1] if winning else legal[0]65 66 67class AdversarialBot(BaseBot):68 def choose_bid(self, game: SpadesGame, player: int) -> int:69 hand = game.hands[player]70 high = len([c for c in hand if c.rank >= 11])71 spades = len([c for c in hand if c.suit == "S"])72 return max(1, min(10, int(round((high + spades * 0.7) / 2.2))))73 74 def choose_card(self, game: SpadesGame, player: int) -> Card:75 legal = sorted(game.legal_cards(player))76 winning = _winning_candidates(game, legal)77 if winning:78 return winning[0]79 return legal[-1]80 81 82class ExpertPolicy(BaseBot):83 def choose_bid(self, game: SpadesGame, player: int) -> int:84 hand = game.hands[player]85 expected = 0.086 suit_counts = {s: len([c for c in hand if c.suit == s]) for s in ("C", "D", "H", "S")}87 for card in hand:88 if card.suit == "S":89 if card.rank >= 14:90 expected += 1.091 elif card.rank >= 13:92 expected += 0.893 elif card.rank >= 12:94 expected += 0.695 elif card.rank >= 11:96 expected += 0.4597 elif card.rank >= 9:98 expected += 0.2599 else:100 expected += 0.1101 elif card.rank == 14:102 expected += 0.8103 elif card.rank == 13:104 expected += 0.45 if suit_counts[card.suit] <= 2 else 0.6105 elif card.rank == 12:106 expected += 0.2 if suit_counts[card.suit] <= 2 else 0.35107 partner = (player + 2) % 4108 partner_bid = game.bid_history[partner] if game.bid_history[partner] is not None else 3109 aggression = 0.0 if partner_bid <= 3 else -0.4110 # Conservative bids reduce frequent contract crashes during long matches.111 return max(1, min(8, int(round(expected + aggression))))112 113 def choose_card(self, game: SpadesGame, player: int) -> Card:114 legal = sorted(game.legal_cards(player))115 if not game.trick:116 return self._lead_card(game, player, legal)117 return self._follow_card(game, player, legal)118 119 def _lead_card(self, game: SpadesGame, player: int, legal: List[Card]) -> Card:120 team = team_for_player(player)121 team_target = _team_target(game, team)122 team_tricks = game.tricks_won[team]123 remaining = len(game.hands[player])124 need = max(team_target - team_tricks, 0)125 if need >= remaining // 2:126 return legal[-1]127 return legal[0]128 129 def _follow_card(self, game: SpadesGame, player: int, legal: List[Card]) -> Card:130 winning = _winning_candidates(game, legal)131 team = team_for_player(player)132 opp = 1 - team133 team_need = max(_team_target(game, team) - game.tricks_won[team], 0)134 opp_need = max(_team_target(game, opp) - game.tricks_won[opp], 0)135 if winning:136 if team_need > opp_need:137 return winning[0]138 return winning[-1]139 if game.trick and any(c.suit == "S" for _, c in game.trick):140 return legal[0]141 return legal[-1]142 143 144def _team_target(game: SpadesGame, team: int) -> int:145 if team == 0:146 return (game.bid_history[0] or 0) + (game.bid_history[2] or 0)147 return (game.bid_history[1] or 0) + (game.bid_history[3] or 0)148 149 150def _winning_candidates(game: SpadesGame, legal_cards: List[Card]) -> List[Card]:151 if not game.trick:152 return []153 trial = []154 for card in legal_cards:155 copy = list(game.trick)156 copy.append((game.current_player, card))157 winner = _winner(copy)158 if winner == game.current_player:159 trial.append(card)160 return trial161 162 163def _winner(trick):164 lead_suit = trick[0][1].suit165 best_player, best_card = trick[0]166 for player, card in trick[1:]:167 if card.suit == "S" and best_card.suit != "S":168 best_player, best_card = player, card169 elif card.suit == best_card.suit and card.rank > best_card.rank:170 best_player, best_card = player, card171 elif best_card.suit != "S" and card.suit == lead_suit and card.rank > best_card.rank:172 best_player, best_card = player, card173 return best_player174 