Skip to content

Instantly share code, notes, and snippets.

@emauton
Created July 28, 2019 13:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emauton/1b51dd77bdcaada225fe2b1b05f0d008 to your computer and use it in GitHub Desktop.
Save emauton/1b51dd77bdcaada225fe2b1b05f0d008 to your computer and use it in GitHub Desktop.
Draw script for #coffee_friends on https://irishtechcommunity.com/
#!/usr/bin/env python3
# Draw script for #coffee_friends on https://irishtechcommunity.com/
# Produces a random draw of pairs from commandline args, using accompanying
# historical data in `pairs.txt` to minimize the number of times the same
# #coffee_friends meet.
import argparse
import itertools
import random
import sys
def generate_candidate_pairs(friends):
'''Given a list of `friends`, generate all unique (x, y) pairs'''
return list(itertools.combinations(sorted(friends), 2))
def generate_draws(pairs):
'''Given a list of unique pairs, return all possible draws
The pairs represent "draws from a hat"; we recursively generate all
further draws possible once we pick one out, etc.
Yes, this could get expensive. No, we don't care: #coffee_friends is smol!
Returns a list of draws `[[(a, b), (c, d)], ...]`'''
if len(pairs) == 1: # base case
return [pairs]
# A little ugly: we pick the first element `a` of the first pair & then
# recurse over each pair `(a, X)`
first = pairs[0][0]
tries = filter(lambda p: p[0] == first, pairs)
results = []
for a, b in tries:
remainder = [p for p in pairs
if a not in p
and b not in p]
for draws in generate_draws(remainder):
results.append([(a, b)] + draws)
return results
def get_pairs_data(filename):
'''Given a file of comma-separated pairs, return a list of pair tuples'''
try:
with open(filename) as f:
pairs = [tuple(sorted(l.strip().split(','))) for l in f]
except Exception as e:
print(f'Failed to load former pairs data ({filename}): {e}')
pairs = []
return pairs
def minimize_rematches(draws, former_pairs):
'''Get a sublist of draws such that "re-matching" is minimized
Given a list of draws + former pairs, return the sublist with minimal
number of friends being re-matched.'''
scored = {}
minimum = sys.maxsize
for d in draws:
score = 0
for pair in d:
if pair in former_pairs:
score += 1
scored[tuple(d)] = score
minimum = min(score, minimum)
return [list(d) for d in scored if scored[d] == minimum]
def get_args(argv):
desc = 'Draw script for #coffee_friends on https://irishtechcommunity.com/'
epilog = ('If you pass an odd number of friends, we add a "Dummy" partner '
'to make up numbers; just add whomever is paired with "Dummy" '
'to one of the other drawn pairs.')
parser = argparse.ArgumentParser(description=desc, epilog=epilog)
parser.add_argument('friends', metavar='@friend', nargs='+',
help='The friends to pair up, using Slack @names')
parser.add_argument('--pairs', default='pairs.txt',
help='Path to comma-separated pairs that met before')
parser.add_argument('--all', action='store_true', default=False,
help='Print all minimized draws vs. picking one')
return parser.parse_args(args=argv)
def main():
args = get_args(sys.argv[1:])
if len(args.friends) % 2 != 0:
args.friends.append('Dummy') # like in Bridge, you see
former_pairs = get_pairs_data(args.pairs)
candidate_pairs = generate_candidate_pairs(args.friends)
draws = generate_draws(candidate_pairs)
minimized = minimize_rematches(draws, former_pairs)
if args.all:
draws = minimized
else:
draws = [random.choice(minimized)]
print('Draw:')
print('=====')
for draw in draws:
for p, q in draw:
print(f'{p} and {q}')
print('-----')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment