Add opponent denial strategy to AI decision making

AI now considers the next player's visible cards before discarding:
- Checks if discarding would give opponent a pair opportunity
- Calculates denial value based on card value and game phase
- May keep a worse card to deny opponent when cost is acceptable
- Denial threshold varies by AI personality (aggression)

Also updates simulation to recognize denial as a valid reason for
swapping good cards, preventing false "swapped good for bad" flags.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-02-06 19:15:39 -05:00
parent cd05930b69
commit 9b53e51aa3
2 changed files with 380 additions and 18 deletions

View File

@@ -342,9 +342,24 @@ def run_cpu_turn(
# 2. We're putting a worse card in
# 3. We're NOT creating a pair (pairing is a valid reason to replace a good card)
# 4. We're NOT in a forced-swap-from-discard situation
# 5. We're NOT denying the next opponent a pair (strategic denial)
creates_pair = partner.face_up and partner.rank == drawn.rank
# Check if this was a denial move (next player has unpaired visible card of drawn rank)
is_denial_move = False
current_idx = next((i for i, p in enumerate(game.players) if p.id == player.id), 0)
next_idx = (current_idx + 1) % len(game.players)
next_player = game.players[next_idx]
for i, opp_card in enumerate(next_player.cards):
if opp_card.face_up and opp_card.rank == drawn.rank:
opp_partner_pos = get_column_partner_position(i)
opp_partner = next_player.cards[opp_partner_pos]
if not (opp_partner.face_up and opp_partner.rank == drawn.rank):
is_denial_move = True
break
if old_card.face_up and old_val < drawn_val and old_val <= 1:
if not creates_pair:
if not creates_pair and not is_denial_move:
stats.record_dumb_move("swapped_good_for_bad")
# Check for dumb move: creating bad pair with negative card