Skip to content

Instantly share code, notes, and snippets.

@jamesmaa
Created January 6, 2018 05:57
Show Gist options
  • Save jamesmaa/1763f52cce6d618ef6a6346c569d3511 to your computer and use it in GitHub Desktop.
Save jamesmaa/1763f52cce6d618ef6a6346c569d3511 to your computer and use it in GitHub Desktop.
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