Update 2-25-25

This commit is contained in:
Donald Calloway 2025-09-25 09:10:40 -07:00
parent f066fe8bb1
commit 199414a6ad

View File

@ -6,6 +6,8 @@ import time
# Define card ranks and their order for runs
RANK_ORDER = {str(n): n for n in range(2, 11)}
RANK_ORDER.update({"A": 1, "J": 11, "Q": 12, "K": 13})
# Card, Deck, DiscardPile, Hand, Player, Game classes
class Card:
suits = ['', '', '', '']
ranks = list(range(1, 11)) + ['J', 'Q', 'K']
@ -45,7 +47,6 @@ class DiscardPile:
self.cards.append(card)
class Hand:
def __init__(self):
self.cards = []
@ -57,13 +58,57 @@ class Hand:
self.cards.remove(card)
def get_all_melds(self):
"""Returns all valid sets and runs from current hand."""
return find_sets(self.cards) + find_runs(self.cards)
return self.find_sets() + self.find_runs()
def find_sets(self):
groups = defaultdict(list)
for c in self.cards:
groups[c.rank].append(c)
return [group for group in groups.values() if len(group) >= 3]
def find_runs(self):
runs = []
suits = defaultdict(list)
for c in self.cards:
suits[c.suit].append(c)
for suit, suited_cards in suits.items():
sorted_cards = sorted(suited_cards, key=lambda c: RANK_ORDER[str(c.rank)])
temp = [sorted_cards[0]]
for i in range(1, len(sorted_cards)):
prev = RANK_ORDER[str(sorted_cards[i - 1].rank)]
curr = RANK_ORDER[str(sorted_cards[i].rank)]
if curr == prev + 1:
temp.append(sorted_cards[i])
else:
if len(temp) >= 3:
runs.append(temp)
temp = [sorted_cards[i]]
if len(temp) >= 3:
runs.append(temp)
return runs
def non_overlapping_meld_combos(self, all_melds):
valid_combos = []
for r in range(1, len(all_melds) + 1):
for combo in combinations(all_melds, r):
used = set()
overlap = False
for meld in combo:
for card in meld:
if card in used:
overlap = True
break
used.add(card)
if overlap:
break
if not overlap:
valid_combos.append(combo)
return valid_combos
def best_meld_combo(self):
"""Returns the optimal non-overlapping meld combination."""
all_melds = self.get_all_melds()
combos = non_overlapping_meld_combos(all_melds)
combos = self.non_overlapping_meld_combos(all_melds)
best = max(combos, key=lambda combo: len(set(c for meld in combo for c in meld)), default=[])
return best
@ -83,6 +128,7 @@ class Player:
def __init__(self, name):
self.name = name
self.hand = Hand()
self.seen_discards = []
def draw(self, deck, discard_pile, from_discard=False):
card = discard_pile.top() if from_discard else deck.draw()
@ -95,13 +141,38 @@ class Player:
def discard(self, discard_pile, card):
self.hand.remove(card)
discard_pile.discard(card)
self.seen_discards.append(card)
def choose_discard(self, discard_pile):
hand = self.hand.cards
best_melds = self.hand.best_meld_combo()
used = set(c for meld in best_melds for c in meld)
candidates = [c for c in hand if c not in used]
# Avoid discarding cards that match recent discards (opponent may want them back)
risky_ranks = {c.rank for c in self.seen_discards[-5:]} # last few discards
risky_suits = {c.suit for c in self.seen_discards[-5:]}
def score(card):
value_penalty = card.value
suit_cluster = sum(1 for c in hand if c.suit == card.suit)
rank_cluster = sum(1 for c in hand if abs(RANK_ORDER[str(c.rank)] - RANK_ORDER[str(card.rank)]) <= 2)
risk_penalty = 5 if card.rank in risky_ranks or card.suit in risky_suits else 0
return value_penalty + risk_penalty - (suit_cluster + rank_cluster)
# Rank candidates by value and meld potential
if candidates:
return min(candidates, key=score)
else:
return max(hand, key=lambda c: c.value)
class Game:
def __init__(self, players):
self.deck = Deck()
self.discard_pile = DiscardPile()
self.players = players
self.scores = {p.name: 0 for p in players}
def deal(self):
for _ in range(10):
@ -110,13 +181,59 @@ class Game:
self.discard_pile.discard(self.deck.draw())
def play_round(self):
# placeholder loop
self.deal()
print("Initial hands:")
for p in self.players:
print(f"{p.name}'s {p.hand}")
turn = 0
while True:
player = self.players[turn % 2]
print(f"\n{player.name}'s turn")
self.take_turn(player)
if player.hand.deadwood_points() == 0:
print(f"{player.name} goes GIN!")
self.score_round(player, gin=True)
break
elif player.hand.deadwood_points() <= 10:
print(f"{player.name} knocks!")
self.score_round(player, gin=False)
break
turn += 1
def take_turn(self, player):
# Simple AI: always draw from stock
drawn = player.draw(self.deck, self.discard_pile, from_discard=False)
print(f"{player.name} draws {drawn}")
# Discard highest deadwood card
deadwood = player.hand.deadwood()
if deadwood:
discard = max(deadwood, key=lambda c: c.value)
player.discard(self.discard_pile, discard)
print(f"{player.name} discards {discard}")
def score_round(self, knocker, gin=False):
opponent = [p for p in self.players if p != knocker][0]
knocker_deadwood = knocker.hand.deadwood_points()
opponent_deadwood = opponent.hand.deadwood_points()
if gin:
score = opponent_deadwood + 25
elif opponent_deadwood <= knocker_deadwood:
score = (knocker_deadwood - opponent_deadwood) + 25 # undercut
self.scores[opponent.name] += score
print(f"{opponent.name} undercuts! Scores {score}")
return
else:
score = opponent_deadwood - knocker_deadwood
self.scores[knocker.name] += score
print(f"{knocker.name} scores {score}")
print("\nScores:")
for name, pts in self.scores.items():
print(f"{name}: {pts}")
# Functions to detect melds outside classes
# Function to detect sets
def find_sets(cards):
groups = defaultdict(list)