set-solver / src /solver /set_finder.py
Tian Wang
Deploy Set Solver web app
8a34385
"""
Set-finding algorithm.
A valid Set consists of 3 cards where, for each attribute,
the values are either ALL THE SAME or ALL DIFFERENT.
"""
from dataclasses import dataclass
from enum import IntEnum
from itertools import combinations
from typing import List, Tuple
class Shape(IntEnum):
DIAMOND = 0
OVAL = 1
SQUIGGLE = 2
class Color(IntEnum):
RED = 0
GREEN = 1
PURPLE = 2
class Number(IntEnum):
ONE = 0
TWO = 1
THREE = 2
class Fill(IntEnum):
SOLID = 0
STRIPED = 1
EMPTY = 2
@dataclass
class Card:
"""A Set card with 4 attributes."""
shape: Shape
color: Color
number: Number
fill: Fill
# Optional: position in image (for visualization)
bbox: Tuple[float, float, float, float] = None # x, y, w, h
def __hash__(self):
return hash((self.shape, self.color, self.number, self.fill))
def __eq__(self, other):
if not isinstance(other, Card):
return False
return (self.shape == other.shape and
self.color == other.color and
self.number == other.number and
self.fill == other.fill)
def to_tuple(self) -> Tuple[int, int, int, int]:
"""Return attributes as tuple of ints."""
return (self.shape, self.color, self.number, self.fill)
@classmethod
def from_tuple(cls, attrs: Tuple[int, int, int, int], bbox=None) -> "Card":
"""Create card from tuple of attribute indices."""
return cls(
shape=Shape(attrs[0]),
color=Color(attrs[1]),
number=Number(attrs[2]),
fill=Fill(attrs[3]),
bbox=bbox
)
def __repr__(self):
n = ["one", "two", "three"][self.number]
return f"{n} {self.fill.name.lower()} {self.color.name.lower()} {self.shape.name.lower()}(s)"
def is_valid_set(card1: Card, card2: Card, card3: Card) -> bool:
"""
Check if three cards form a valid Set.
For each attribute, the three values must be either:
- All the same (e.g., all red)
- All different (e.g., red, green, purple)
"""
for attr in ['shape', 'color', 'number', 'fill']:
values = [getattr(card1, attr), getattr(card2, attr), getattr(card3, attr)]
unique = len(set(values))
# Valid: all same (1 unique) or all different (3 unique)
# Invalid: exactly 2 unique
if unique == 2:
return False
return True
def find_all_sets(cards: List[Card]) -> List[Tuple[Card, Card, Card]]:
"""
Find all valid Sets among the given cards.
Uses brute force: check all C(n,3) combinations.
For 12 cards: C(12,3) = 220 combinations - very fast.
For 21 cards (max in real game): C(21,3) = 1330 combinations - still fast.
"""
valid_sets = []
for combo in combinations(cards, 3):
if is_valid_set(*combo):
valid_sets.append(combo)
return valid_sets
def find_first_set(cards: List[Card]) -> Tuple[Card, Card, Card] | None:
"""Find the first valid Set, or None if no Set exists."""
for combo in combinations(cards, 3):
if is_valid_set(*combo):
return combo
return None
# --- Utilities ---
def generate_all_cards() -> List[Card]:
"""Generate all 81 unique Set cards."""
cards = []
for s in Shape:
for c in Color:
for n in Number:
for f in Fill:
cards.append(Card(shape=s, color=c, number=n, fill=f))
return cards
def card_to_index(card: Card) -> int:
"""Convert card to unique index (0-80)."""
return (card.shape * 27 + card.color * 9 + card.number * 3 + card.fill)
def index_to_card(idx: int) -> Card:
"""Convert index (0-80) to card."""
fill = idx % 3
idx //= 3
number = idx % 3
idx //= 3
color = idx % 3
idx //= 3
shape = idx
return Card(Shape(shape), Color(color), Number(number), Fill(fill))
# --- Demo ---
if __name__ == "__main__":
# Example: find sets in a random deal
import random
all_cards = generate_all_cards()
print(f"Total cards in deck: {len(all_cards)}")
# Deal 12 cards
deal = random.sample(all_cards, 12)
print(f"\nDealt {len(deal)} cards:")
for i, card in enumerate(deal):
print(f" {i+1}. {card}")
# Find all sets
sets = find_all_sets(deal)
print(f"\nFound {len(sets)} valid Set(s):")
for i, (c1, c2, c3) in enumerate(sets):
print(f"\n Set {i+1}:")
print(f" - {c1}")
print(f" - {c2}")
print(f" - {c3}")