Skip to content

Instantly share code, notes, and snippets.

@shidarin
Last active August 29, 2015 14:25
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 shidarin/d3c476fe53f8bc998208 to your computer and use it in GitHub Desktop.
Save shidarin/d3c476fe53f8bc998208 to your computer and use it in GitHub Desktop.
Script used to read a quiz results for /r/TheExpanse in CSV and grade, then select winners.
#!/usr/bin/python
"""Script used to read a quiz results CSV and grade, then select winners."""
#==============================================================================
# IMPORTS
#==============================================================================
import csv
from datetime import datetime
import random
import sys
#==============================================================================
# GLOBALS
#==============================================================================
ANSWER_KEY = {
# Page numbers reference First Edition, 2011
'author_1': 'Ty Franck',
'author_2': 'Daniel Abraham',
'honor_among_thieves': [
'Star Wars books always get New York Times Bestseller', # Signing Q&A
"It's friggin Star Wars" # It is known.
],
'ty_job': 'Assistant to George RR Martin',
'daniel_middle_name': 'James', # Wikipedia
'julie_mao_prison': 'The Anubis', # This is the question JASC corrected.
'protogen_slogan': 'First Fastest Furthest.', # p342
'holden_ranch': 'Montana, Earth',
'rocinante': "The name of Don Quixote's horse", # Wikipedia
'cant_capt': 'McDowell', # p15
'flophouse_name': 'Lionel Polanski', # p220
'proto_growth': 'Radiation', # p255
'eros_security': 'Carne Por la Machina', # p228
'thothe': 'Dresdon', # p410
'UNN_ship': 'Ravi', # p549
'charges': [ # p549
'Interfering with UNN military operations',
'Unlawfully commandeering UNN military assets'
]
}
#==============================================================================
# CLASSES
#==============================================================================
class Entry(object):
"""A single entry in the contest."""
entries = {}
def __init__(self, entry):
"""Initialize the entry with all answers, using a dictionary.
Sample `entry` (not all correct answers) is as follows::
{'Who is the new security firm on Eros?':
'Carne Por la Machina',
'The UNN ship chasing Eros with the Roci is named what?':
'Ravi',
'In Don Quixote, Rocinante is...':
"The name of Don Quixote's horse",
'James SA Corey wrote Star Wars: Honor Among Thieves because...':
"It's friggin Star Wars",
'Julie Mao is held prisoner aboard what ship?':
'Unnamed stealth ship',
'If your preferred prize is unavailable,
do you want the other prize?':
'Yes',
'Timestamp':
'7/14/2015 14:01:28',
"The captain of the Canterbury's name is...":
'McDowell',
'Are you over 18?':
'Yes',
'What makes the protomolecule grow?':
'Radiation',
'Who runs Thothe station?':
'Dresdon',
"Protogen's secret slogan is...":
'First Fastest Furthest.',
"James Holden's family lives where?":
'Montana, Earth',
'Who is Author #1?':
'Ty Franck',
"What's your reddit username?":
'backstept',
'Which do you prefer- Drive or google cardboard?':
'Drive',
'Who is Author #2?':
'Daniel Abraham',
"Daniel Abraham's middle name is...":
'Corey',
"One of Ty Franck's previous jobs was...":
'Assistant to George RR Martin',
'The name Julie Mao checks in to a flophouse on Eros Under is...':
'Lionel Polanski',
'What does the captain of the above ship charge Holden with?':
'Unlawful occupation of MCRN military assets'}
"""
self._username = entry[
"What's your reddit username?"
].replace('/u/', '')
if self._username in self.entries:
raise ValueError(
"User {0} has entered more than once! Only the first will "
"be counted.".format(
self._username
))
if self._username == 'shidarin':
raise ValueError("Shidarin is not allowed to enter.")
self._adult = entry['Are you over 18?']
if self._adult == 'No':
raise ValueError("User {0} is not over 18!".format(self._username))
self._time = self._convert_time(entry['Timestamp'])
if self._time > datetime(month=7, day=20, year=2015):
raise ValueError("User {0} entered after contest was over!")
self._author_1 = entry['Who is Author #1?']
self._author_2 = entry['Who is Author #2?']
self._honor_among_thieves = entry[
'James SA Corey wrote Star Wars: Honor Among Thieves because...'
]
self._ty_job = entry["One of Ty Franck's previous jobs was..."]
self._daniel_middle_name = entry["Daniel Abraham's middle name is..."]
self._julie_mao_prison = entry[
'Julie Mao is held prisoner aboard what ship?'
]
self._protogen_slogan = entry["Protogen's secret slogan is..."]
self._holden_ranch = entry["James Holden's family lives where?"]
self._rocinante = entry['In Don Quixote, Rocinante is...']
self._cant_capt = entry["The captain of the Canterbury's name is..."]
self._flophouse_name = entry[
'The name Julie Mao checks in to a flophouse on Eros Under is...'
]
self._proto_growth = entry['What makes the protomolecule grow?']
self._eros_security = entry['Who is the new security firm on Eros?']
self._thothe = entry['Who runs Thothe station?']
self._UNN_ship = entry[
'The UNN ship chasing Eros with the Roci is named what?'
]
self._charges = entry[
'What does the captain of the above ship charge Holden with?'
].split(', ')
self._prize_preference = entry[
'Which do you prefer- Drive or google cardboard?'
]
self._preferred_prize_fallback = entry[
'If your preferred prize is unavailable, '
'do you want the other prize?'
]
self._score = None
self.entries[self._username] = self
# Special Methods
def __repr__(self):
return "<{cls}: {username}>".format(
cls=self.__class__.__name__,
username=self.username
)
# Properties
@property
def adult(self):
"""We raise a value error during init if this is false, but..."""
return True if self._adult == 'Yes' else False
@property
def preferred_prize_fallback(self):
return True if self._preferred_prize_fallback == 'Yes' else False
@property
def prize_preference(self):
return self._prize_preference
@property
def score(self):
if not self._score:
self._score = self._score_entry()
return self._score
@property
def time(self):
return self._time
@property
def username(self):
return self._username
# Private Methods
@staticmethod
def _convert_time(timestamp):
"""Convert a timestamp string to datetime object.
Sample timestamp: '7/14/2015 14:01:28'
"""
return datetime.strptime(timestamp, '%m/%d/%Y %H:%M:%S')
def _score_entry(self):
"""Score the entry according to the answer key."""
score = 0
score += self._score_single(self._author_1, 'author_1')
score += self._score_single(self._author_2, 'author_2')
score += self._score_single(
self._honor_among_thieves, 'honor_among_thieves'
)
score += self._score_single(self._ty_job, 'ty_job')
score += self._score_single(
self._daniel_middle_name, 'daniel_middle_name'
)
if self.time > datetime(month=7, day=14, year=2015, hour=16):
score += self._score_single(
self._julie_mao_prison, 'julie_mao_prison'
)
else:
# People who entered the contest before the datetime above did not
# actually have a correct answer to this question, so all
# will be credited.
score += 1
score += self._score_single(self._protogen_slogan, 'protogen_slogan')
score += self._score_single(self._holden_ranch, 'holden_ranch')
score += self._score_single(self._rocinante, "rocinante")
score += self._score_single(self._cant_capt, 'cant_capt')
score += self._score_single(self._flophouse_name, 'flophouse_name')
score += self._score_single(self._proto_growth, 'proto_growth')
score += self._score_single(self._eros_security, 'eros_security')
score += self._score_single(self._thothe, 'thothe')
score += self._score_single(self._UNN_ship, 'UNN_ship')
score += self._score_checkbox(self._charges, 'charges')
return score
@staticmethod
def _score_single(answer, key):
"""Score a single answer against the ANSWER_KEY."""
return 1 if answer in ANSWER_KEY[key] else 0
@staticmethod
def _score_checkbox(answer, key):
"""Score a multiple possible answer against the ANSWER_KEY."""
score = 0
for ans in answer:
if ans in ANSWER_KEY[key]:
score += 0.5
return score
#==============================================================================
# PRIVATE METHODS
#==============================================================================
def _print_winner_line(winner):
print "{user}: {score} - {preferred}.{sub}".format(
user=winner.username,
score=winner.score,
preferred=winner.prize_preference,
sub=' No substitutions' if not winner.preferred_prize_fallback else ''
)
#==============================================================================
# MAIN
#==============================================================================
def main():
"""Main script."""
csv_file = sys.argv[1]
with open(csv_file, 'r') as f:
rows = csv.DictReader(f)
for entry in rows:
try:
Entry(entry)
except ValueError as err:
print "Invalid entry:", err
print "\nReceived the following entries and scores:"
entries = sorted(
Entry.entries.values(), key=lambda x: x.score, reverse=True
)
for entry in entries:
print entry.username, ':', entry.score
top_50_percent = entries[:len(entries)/2] # Grab the top half of entries
low_score = top_50_percent[-1].score # Lowest score is last entry.
for entry in entries:
# We need to make sure we include all entries that tie with the
# lowest scoring entry in the cutoff.
if entry.score == low_score and entry not in top_50_percent:
top_50_percent.append(entry)
print "\nCut off score for top 50 percent is {score}.".format(
score=low_score
)
winners = []
while len(winners) < 10:
# Keep randomly selecting winners until we have 10.
winner = random.choice(top_50_percent)
if winner not in winners:
winners.append(winner)
winners = sorted(winners, key=lambda x: x.score, reverse=True)
for winner in winners:
# Remove so they are not included in fallback calculation
top_50_percent.remove(winner)
_print_winner_line(winner)
print '\nFallbacks are:'
# The top 5 scoring entries who were not randomly selected will be used as
# fallbacks in the order of their scoring.
for fallback in top_50_percent[:5]:
_print_winner_line(fallback)
# At this point, the scorer should be able to easily figure out who gets
# what prize. We could go a step further and have the program do it,
# but why over engineer this more than it already is?
#==============================================================================
# GATE
#==============================================================================
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment