Set Card Game Simulations
from itertools import product, combinations | |
from random import shuffle | |
from collections import OrderedDict, Counter | |
def is_set(c1, c2, c3): | |
for i in range(4): | |
if c1[i] == c2[i] == c3[i]: | |
continue | |
elif c1[i] != c2[i] and c2[i] != c3[i] and c3[i] != c1[i]: | |
continue | |
else: | |
return False | |
return True | |
def has_set_left(field): | |
for cards_3 in combinations(field, 3): | |
c1, c2, c3 = cards_3 | |
if is_set(c1, c2, c3): | |
return True | |
return False | |
def missing_digit(char1, char2): | |
if char1 == char2: | |
return char1 | |
return str((int(char1, 3) ^ int(char2, 3)) ^ 3) | |
def calculate_c3(c1, c2): | |
return ''.join([missing_digit(char1, char2) for char1, char2 in zip(c1, c2)]) | |
def reversed_combinations(field): | |
last_3 = field[-3:] | |
combos = list(combinations(field, 2)) | |
return sorted(combos, key=lambda combo: (combo[0] in last_3) + (combo[1] in last_3), reverse=True) | |
def max_mode(field): | |
counts = [] | |
for i in range(4): | |
most_common_count = Counter([card[i] for card in field]).most_common(1)[0][1] | |
counts.append(most_common_count) | |
return max(counts) | |
def min_prop(field): | |
counts = [] | |
for i in range(4): | |
least_common = Counter([card[i] for card in field]).most_common()[-1][1] | |
counts.append(least_common) | |
return min(counts) | |
class SetGame(object): | |
def __init__(self): | |
self.deck = [''.join(digits) for digits in product('012', repeat=4)] | |
shuffle(self.deck) | |
# self.field = [] | |
# while not has_set_left(self.field): | |
# self.add_new_cards(1) | |
self.field = [self.deck.pop() for _ in range(12)] | |
self.solved = False | |
def add_new_cards(self, num=3): | |
cards_to_draw = min(num, len(self.deck)) | |
self.field = self.field + [self.deck.pop() for _ in range(cards_to_draw)] | |
if not self.deck and not has_set_left(self.field): | |
self.solved = True | |
def call_set(self, c1, c2, c3): | |
if c1 in self.field and c2 in self.field and c3 in self.field: | |
if is_set(c1, c2, c3): | |
# Remove cards from field | |
self.field.pop(self.field.index(c1)) | |
self.field.pop(self.field.index(c2)) | |
self.field.pop(self.field.index(c3)) | |
return True | |
raise Exception("Not a set!") | |
def play_game(self, *agents): | |
scores = [0] * len(agents) | |
while not self.solved: | |
found_sets = [agent.find_set(self) for agent in agents] | |
# print(found_sets) | |
if not any(found_sets): | |
self.add_new_cards() | |
continue | |
mental_cycles_spent = [agent.mental_cycles for agent in agents] | |
turn_winner_index = mental_cycles_spent.index(min(mental_cycles_spent)) | |
c1, c2, c3 = found_sets[turn_winner_index] | |
self.call_set(c1, c2, c3) | |
scores[turn_winner_index] += 1 | |
[agent.reset() for agent in agents] | |
# print("Scores " + str(scores)) | |
return scores.index(max(scores)) | |
def mental_cycles(num): | |
def dec(fn): | |
def method(*args, **kwargs): | |
self = args[0] | |
res = fn(*args, **kwargs) | |
self.mental_cycles += num | |
return res | |
return method | |
return dec | |
class Agent(object): | |
def __init__(self): | |
self.mental_cycles = 0 | |
self.seen_states = set() | |
def solve_game(self, game): | |
while not game.solved: | |
cards = self.find_set(game) | |
if not cards: | |
game.add_new_cards() | |
self.reset_seen_states() | |
else: | |
c1, c2, c3 = cards | |
game.call_set(c1, c2, c3) | |
# game.add_new_cards() | |
mc = self.mental_cycles | |
self.reset_mental_cycles() | |
return mc | |
def reset(self): | |
self.reset_mental_cycles() | |
self.reset_seen_states() | |
def reset_mental_cycles(self): | |
self.mental_cycles = 0 | |
def reset_seen_states(self): | |
self.seen_states = set() | |
class NaiveAgent(Agent): | |
""" | |
Finds 3 cards and tests if they are a set | |
""" | |
def find_set(self, game): | |
combos = list(combinations(game.field, 3)) | |
shuffle(combos) | |
for cards_3 in combos: | |
if cards_3 in self.seen_states: | |
continue | |
self.seen_states.add(cards_3) | |
c1, c2, c3 = cards_3 | |
if self.is_set(c1, c2, c3): | |
return c1, c2, c3 | |
else: | |
return None | |
game.add_new_cards() | |
@mental_cycles(0.75) | |
def is_set(self, c1, c2, c3): | |
return is_set(c1, c2, c3) | |
class Pick2RandomAgent(Agent): | |
""" | |
Pick 2 cards randomly and searches for the 3rd card | |
""" | |
def find_set(self, game): | |
combos = list(combinations(game.field, 2)) | |
shuffle(combos) | |
for cards_2 in combos: | |
if cards_2 in self.seen_states: | |
continue | |
self.seen_states.add(cards_2) | |
c1, c2 = cards_2 | |
c3 = self.calculate_c3(c1, c2) | |
if self.find_card(game.field, c3): | |
return c1, c2, c3 | |
else: | |
return None | |
@mental_cycles(1) | |
def calculate_c3(self, c1, c2): | |
return calculate_c3(c1, c2) | |
@mental_cycles(0.5) | |
def find_card(self, field, card): | |
return card in field | |
class Pick2CardsLastAgent(Agent): | |
""" | |
Picks two cards starting from the most recently inserted cards | |
""" | |
def find_set(self, game): | |
for cards_2 in reversed_combinations(game.field): | |
if cards_2 in self.seen_states: | |
continue | |
self.seen_states.add(cards_2) | |
c1, c2 = cards_2 | |
c3 = self.calculate_c3(c1, c2) | |
if self.find_card(game.field, c3): | |
return c1, c2, c3 | |
else: | |
return None | |
@mental_cycles(1) | |
def calculate_c3(self, c1, c2): | |
return calculate_c3(c1, c2) | |
@mental_cycles(0.5) | |
def find_card(self, field, card): | |
return card in field | |
class PropertyFilteringAgent(Agent): | |
def find_set(self, game): | |
combos = list(combinations(game.field, 2)) | |
shuffle(combos) | |
for cards_2 in combos: | |
if cards_2 in self.seen_states: | |
continue | |
self.seen_states.add(cards_2) | |
c1, c2 = cards_2 | |
cards = game.field[:] | |
for i in range(4): | |
val = self.missing_digit(c1[i], c2[i]) | |
cards = self.scan_for_prop(cards, i, val, cards_2) | |
if len(cards) == 1: | |
if self.is_set(c1, c2, cards[0]): | |
return c1, c2, cards[0] | |
else: | |
break | |
elif len(cards) < 1: | |
break | |
else: | |
return None | |
@mental_cycles(0.3) | |
def missing_digit(self, char1, char2): | |
return missing_digit(char1, char2) | |
@mental_cycles(0.3) | |
def scan_for_prop(self, field, index, val, excluding): | |
return [card for card in field if card not in excluding and card[index] == val] | |
@mental_cycles(0.3) | |
def is_set(self, c1, c2, c3): | |
return is_set(c1, c2, c3) | |
class PropertyFilteringLastCardsAgent(Agent): | |
""" | |
Does property filtering and chooses the 2 cards by the last 3 cards inserted first | |
""" | |
def find_set(self, game): | |
combos = reversed_combinations(game.field) | |
# shuffle(combos) | |
for cards_2 in combos: | |
if cards_2 in self.seen_states: | |
continue | |
self.seen_states.add(cards_2) | |
c1, c2 = cards_2 | |
cards = game.field[:] | |
for i in range(4): | |
val = self.missing_digit(c1[i], c2[i]) | |
cards = self.scan_for_prop(cards, i, val, cards_2) | |
if len(cards) == 1: | |
if self.is_set(c1, c2, cards[0]): | |
return c1, c2, cards[0] | |
break | |
elif len(cards) < 1: | |
break | |
else: | |
return None | |
@mental_cycles(0.3) | |
def missing_digit(self, char1, char2): | |
return missing_digit(char1, char2) | |
@mental_cycles(0.3) | |
def scan_for_prop(self, field, index, val, excluding): | |
return [card for card in field if card not in excluding and card[index] == val] | |
@mental_cycles(0.3) | |
def is_set(self, c1, c2, c3): | |
return is_set(c1, c2, c3) | |
# agent1 = NaiveAgent() | |
# results = [agent1.solve_game(SetGame()) for _ in range(1000)] | |
# print(str(agent1.__class__.__name__) + " avg mental cycles per game " + str(sum(results) / 1000)) | |
# agent2 = Pick2RandomAgent() | |
# results = [agent2.solve_game(SetGame()) for _ in range(1000)] | |
# print(str(agent2.__class__.__name__) + " avg mental cycles per game " + str(sum(results) / 1000)) | |
# agent3 = Pick2CardsLastAgent() | |
# results = [agent3.solve_game(SetGame()) for _ in range(1000)] | |
# print(str(agent3.__class__.__name__) + " avg mental cycles per game " + str(sum(results) / 1000)) | |
# agent4 = PropertyFilteringAgent() | |
# results = [agent4.solve_game(SetGame()) for _ in range(1000)] | |
# print(str(agent4.__class__.__name__) + " avg mental cycles per game " + str(sum(results) / 1000)) | |
# agent5 = PropertyFilteringLastCardsAgent() | |
# results = [agent5.solve_game(SetGame()) for _ in range(1000)] | |
# print(str(agent5.__class__.__name__) + " avg mental cycles per game " + str(sum(results) / 1000)) | |
# winners = Counter([SetGame().play_game( | |
# NaiveAgent(), | |
# Pick2RandomAgent(), | |
# Pick2CardsLastAgent(), | |
# PropertyFilteringAgent(), | |
# PropertyFilteringLastCardsAgent() | |
# ) for _ in range(1000)]) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment