Off-Platform Project: Abruptly Goblins!

Hello! Just in case any fellow learners are looking for a discussion/solution thread for:

8. Learn Python 3 - Dictionaries, Off-Platform Project: Abruptly Goblins

The parameters on the original solution key may throw some of us off, so I tried to refine a version that helps with clarifying the steps.

#Organising Sorcery Society's Game Night
# 1. check availability of players
# 2. check which day most players are available
# 3. informing them of game night

# create a main list to house information on all our existing gamers
gamers = []

# create a function to append gamer entries that have the required keys “name”, “availability”, e.g. {"name":"Kara Danvers", "availability":["Friday", "Saturday", "Sunday"]}
# we'll name the parameters (gamer for 1 single gamer entry) and (valid_gamers for the list we are appending to)

def add_gamer(gamer, valid_gamers):
    if gamer.get("name") and gamer.get("availability”):
        valid_gamers.append(gamer)
    else:
        print("Gamer missing required information”)            

# new gamer entry
kara_danvers = {"name":"Kara Danvers”, "availability":["Friday", "Saturday", "Sunday"]}

# initiate add_gamer function to add new gamer to main list
add_gamer(kara_danvers, gamers)
add_gamer({"name":"Alex Danvers", "availability":["Monday", "Friday"]}, gamers)
add_gamer({"name":"Lena Luthor", "availability":["Saturday", "Sunday"]}, gamers)
add_gamer({"name":"Winnslow Schott", "availability":["Thursday", "Friday", "Saturday"]}, gamers)
add_gamer({"name":"James Olsen", "availability":["Wednesday", "Thursday", "Friday"]}, gamers)
add_gamer({"name":"Kelly Olsen", "availability":["Monday", "Tuesday", "Wednesday"]}, gamers)
add_gamer({"name":"Nia Nal", "availability":["Wednesday", "Thursday", "Friday"]}, gamers)
add_gamer({"name":"Querl Dox", "availability":["Wednesday", "Friday"]}, gamers)
add_gamer({"name":"Barry Allen", "availability":["Tuesday", "Wednesday", "Thursday"]}, gamers)
add_gamer({"name":"Iris West", "availability":["Tuesday", "Thursday"]}, gamers)
#print(gamers)

# build a base day, availabilities counter with the count at 0
def build_daily_frequency_counter():
    return {"Monday":0, "Tuesday":0, "Wednesday":0, "Thursday":0, "Friday":0, "Saturday":0, "Sunday”:0 }

daily_frequency_counter = build_daily_frequency_counter( )

# using the base counter we built, start to iterate the availability of each game and tally the counter
# the parameters are (the main list of gamers) and (the counter we are using)
def calculate_availability(valid_gamers, counter):
    for gamer in valid_gamers:
        for day in gamer['availability']:
            counter[day] += 1

calculate_availability(gamers, daily_frequency_counter)
#print(daily_frequency_counter)                                              

# create a function that picks the best day for game night, with one parameter (the counter)
def find_best_day(counter):
    best_avail = 0 
    for day, availability in counter.items():
        if availability > best_avail:
            best_day = day
            best_avail = availability
    return best_day

game_night = find_best_day(daily_frequency_counter)
print(game_night)

# create a function that generates a list of gamers names available on the day of game night, with the parameters (the main list) and (the best day)
# this is used in the emails send to gamers informing them about game night
def avail_on_day(valid_gamers, best_day):
    avail_gamers = []
    for gamer in valid_gamers:
        for value in gamer.values(): 
            if best_day in value:                                                   
                avail_gamers.append(gamer['name’])                           
    return avail_gamers

    # return [gamer for gamer in valid_gamers if best_day in gamer['availability’]]
    # you can use the above list comprehension if you want the name + availability

avail_game_night = avail_on_day(gamers, game_night)
print(avail_game_night)

# create variable for formatted email blast message
invite_email = """
Dear {name},

The Sorcery Society is happy to host "{game}" night and wishes you will attend. Come by on {day_of_week} and have a blast!

Magically Yours,
The Sorcery Society
"""

# create a function to send an invitation email for those who can make it to the chosen game night day, with the parameters (available attendees), (chosen day) and (the game)
def send_email(avail_attendees, chosen_day, game):
    for gamer in avail_attendees:
        print(invite_email.format(name = gamer, day_of_week = chosen_day, game = game))

send_email(avail_game_night, game_night, "Abruptly Goblins!")


# for gamers who couldn't attend, use list comprehension to sift them from the main list out into a separate list
# gamers who couldn't attend did not have the game night day in their available days
unable_to_attend_best_night = [gamer for gamer in gamers if game_night not in gamer['availability']]

# start a new day availability counter for those who couldn't attend for best day game night
# this counter will start at 0
second_night_avail_counter = build_daily_frequency_counter()
print(second_night_avail_counter)

# call the function that tallies the day availabilities of the remaining gamers
calculate_availability(unable_to_attend_best_night, second_night_avail_counter)
print(second_night_avail_counter)

# call the function to pick the best night tallied by the second counter
second_night = find_best_day(second_night_avail_counter)
print(second_night)

# create list of all the gamers available for the second game night picked
avail_second_game_night = avail_on_day(gamers, second_night)
# call the function to send them a notification email regardless if they are also avail for the best day game night
send_email(avail_second_game_night, second_night, "Abruptly Goblins!")

Questions

  1. What if there are multiple contenders for best day game night? Which day will the system choose or it will be randomised?

  2. Because of the gamers who are unable to attend the best day game night, those gamers who can attend both days will receive another email informing them for second day game night, which may feel like spam.

If we want to send the overlapping gamers 1 email with 2 choice of days, this means we need to pre-select the most avail day, second most avail day before sending the emails. Also, to do this we have to make 3 lists (a. gamers who can only attend most avail day, b. gamers who can only attend second avail day, c. gamers who can attend both most and second avail day (have to be popped from lists a and b). I’m still working on this.

Is there are other ways to better improve this exercise, please do share! :grinning:

when checking for dictionary keys you’d want to do: key in mydict
an example of where your code is arguably wrong is if availability is [] which is falsy, but it does exist. Might not matter to your application because if there’s no availability that’s as good as not existing.

Arbitrary and random are different things, arbitrary would mean that you shouldn’t rely on which it is and accept any, but nobody rolled dice for it, so it’s not random. You may have done something else to affect order or you may get the same order anyway simply because nobody is rolling dice. If you care, make it the way you care for it to be.

2) Yeah instead of sending a mail you might take note of which days you’d want to send mails for, and then when that’s done you’d send one mail to each person which mentions all days. You wouldn’t need three lists for it, but you might want a list for each one associated with their name (a dictionary)

{'bob': [], 'lisa': [1], 'giggles': [3,4]}

…I suppose that’s the same thing. I think it’s better to group things together though, instead of having multiple data structures representing one thing.

1 Like

You could do this with set operations too.
Each day is a set of people that can come.

# not entirely correct
day1 = max(days)
day2 = max(day - day1 for day in days)
sendMailTo = day1 | day2

this loses track of which days is which but … details xD
(some extra code to remember it and it’s still fine)

the data structure would look like this:

{'Friday': {Gamer(name='Alex Danvers', availability=('Monday', 'Friday')),
            Gamer(name='James Olsen', availability=('Wednesday', 'Thursday', 'Friday')),
            Gamer(name='Kara Danvers', availability=('Friday', 'Saturday', 'Sunday')),
            Gamer(name='Nia Nal', availability=('Wednesday', 'Thursday', 'Friday')),
            Gamer(name='Querl Dox', availability=('Wednesday', 'Friday', 'Tuesday')),
            Gamer(name='Winnslow Schott', availability=('Thursday', 'Friday', 'Saturday'))},
 'Monday': {Gamer(name='Alex Danvers', availability=('Monday', 'Friday')),
            Gamer(name='Kelly Olsen', availability=('Monday', 'Tuesday', 'Wednesday'))},
 'Saturday': {Gamer(name='Kara Danvers', availability=('Friday', 'Saturday', 'Sunday')),
              Gamer(name='Lena Luthor', availability=('Saturday', 'Sunday')),
              Gamer(name='Winnslow Schott', availability=('Thursday', 'Friday', 'Saturday'))},
 'Sunday': {Gamer(name='Kara Danvers', availability=('Friday', 'Saturday', 'Sunday')),
            Gamer(name='Lena Luthor', availability=('Saturday', 'Sunday'))},
 'Thursday': {Gamer(name='Barry Allen', availability=('Tuesday', 'Wednesday', 'Thursday')),
              Gamer(name='Iris West', availability=('Tuesday', 'Thursday')),
              Gamer(name='James Olsen', availability=('Wednesday', 'Thursday', 'Friday')),
              Gamer(name='Nia Nal', availability=('Wednesday', 'Thursday', 'Friday')),
              Gamer(name='Winnslow Schott', availability=('Thursday', 'Friday', 'Saturday'))},
 'Tuesday': {Gamer(name='Barry Allen', availability=('Tuesday', 'Wednesday', 'Thursday')),
             Gamer(name='Iris West', availability=('Tuesday', 'Thursday')),
             Gamer(name='Kelly Olsen', availability=('Monday', 'Tuesday', 'Wednesday')),
             Gamer(name='Querl Dox', availability=('Wednesday', 'Friday', 'Tuesday'))},
 'Wednesday': {Gamer(name='Barry Allen', availability=('Tuesday', 'Wednesday', 'Thursday')),
               Gamer(name='James Olsen', availability=('Wednesday', 'Thursday', 'Friday')),
               Gamer(name='Kelly Olsen', availability=('Monday', 'Tuesday', 'Wednesday')),
               Gamer(name='Nia Nal', availability=('Wednesday', 'Thursday', 'Friday')),
               Gamer(name='Querl Dox', availability=('Wednesday', 'Friday', 'Tuesday'))}}
from collections import namedtuple

days = {}

Gamer = namedtuple('Gamer', ['name', 'availability'])


def add_gamer(gamer):
    gamer = Gamer(gamer['name'], tuple(gamer['availability']))
    for day in gamer.availability:
        current = days.get(day, set())
        current.add(gamer)
        days[day] = current

add_gamer({"name":"Kara Danvers", "availability":["Friday", "Saturday", "Sunday"]})
add_gamer({"name":"Alex Danvers", "availability":["Monday", "Friday"]})
add_gamer({"name":"Lena Luthor", "availability":["Saturday", "Sunday"]})
add_gamer({"name":"Winnslow Schott", "availability":["Thursday", "Friday", "Saturday"]})
add_gamer({"name":"James Olsen", "availability":["Wednesday", "Thursday", "Friday"]})
add_gamer({"name":"Kelly Olsen", "availability":["Monday", "Tuesday", "Wednesday"]})
add_gamer({"name":"Nia Nal", "availability":["Wednesday", "Thursday", "Friday"]})
add_gamer({"name":"Querl Dox", "availability":["Wednesday", "Friday", "Tuesday"]})
add_gamer({"name":"Barry Allen", "availability":["Tuesday", "Wednesday", "Thursday"]})
add_gamer({"name":"Iris West", "availability":["Tuesday", "Thursday"]})

invite_email = """\
Dear {name},

The Sorcery Society is happy to host "{game}" night and wishes you will attend. Come by on {day_of_week} and have a blast!

Magically Yours,
The Sorcery Society
"""


def send_email(attendee, days, game):
    print(repr(
        invite_email.format(
            name=gamer, day_of_week=days, game=game
        )
    ))


day1, p1 = max(days.items(),
               key=lambda day: len(day[1]))
day2, p2 = max(((day, p-p1) for day, p in days.items()),
               key=lambda day: len(day[1]))
for gamer in p1 | p2:
    invite_days = [d for d in [day1, day2] if d in gamer.availability]
    send_email(gamer, invite_days, "Abruptly Goblins")
1 Like

Hey ionatan! It’s always a joy to see your response even though it will take me quite a bit of time to digest your solution. I’ll go input the code and take a look at how it works.

Thank you for your effort in mentoring learners like us. :grinning:

PS - May I know how long it took you to reach your current proficiency on python? I am actively managing my expectations on learning the material. :sweat_smile:

mmh no it won’t!
codecademy had you implement a round-about version of max
but max already exists (and is itself a simple thing)

so, max day by head count (plus some set operations to manage groups, things like remove one group from another, or merging two groups)

And since we’re doing max of days, we would need to represent days. So a day is… a name (monday, tuesday …) … plus who comes. <-- so it’s just making the shape of the data fit the operations we want to do

and since it’s max by something, there’s a need for a function that takes a day and says how many comes.

Usually when you have a solid plan for what should happen, the code will work out just fine.

Things go south if the plan doesn’t add up, that won’t be possible to write code for. Or, if one doesn’t stay true to the plan (why would one change it if it makes sense?)

Take a problem. Forget about programs. You’ve got some information, how should you poke at that information to get what you want? Found a path between the start and finish, and you’ve sanity checked it? Go write the code.
You might find out that the plan actually sucked, oh well, try again (back to square 1)
But if your plan is some kind of dead simple, then your odds are pretty good.
Max day by head count <- this will work, obviously.

If you have a plan and you think it might work but it’s complicated, then maybe that’s just saying it should be broken down into simpler parts.
Or you could throw a whole lot of brain power at it, that’s the dumb kind of smart though, better to make the solution dumb, because then it’s easy to be smart enough for it. This is what beginners do. They’re not too dumb for the problem, their solution is too clever.

3 Likes

Thank you for sharing this really, all of what you’ve said is logical and straightforward.

Sometimes, beginners do head into the problem without much thinking about the full plan of the solution, which is counter-productive and sometimes overwhelming. But I’ve observed that it helps to work out the problem with simple methods, then refine the code to remove redundancy later on when the solution is complete. Really appreciate your patience and willingness to help out!

These are the several things I need read up on:

  1. collections module and namedtuple
  2. how set() method works - it converts keys of a dictionary into a tuple list by itself?

set() gives you an empty set, just like how list() gives you an empty list, same with most container types.

>>> str()
''
>>> list()
[]
>>> tuple()
()
>>> set()
set()
>>> dict()
{}
>>> int()
0
>>> float()
0.0

there are set literals: {1, 2, 3} but not for empty {} because that’s a dict

a Set is similar to a dictionary, but has no values. Good for reasoning about memberships in groups

>>> yellow = {'banana', 'sun'}
>>> hot = {'sun'}
>>> yellow - hot  # yellow things that are not hot
{'banana'}
>>> yellow & hot  # things that are both yellow and hot
{'sun'}
1 Like

Ah… Got it!

A dictionary is in a tuple, but has key : value.
A set is also in a tuple, but groups each individual items, and can be used as a main category and subcategories. This is so useful, now I see why you used it as a day 1, 2 gamers, day 1 gamer and day 2 gamer separation!

I’ve tried the code you suggested, this was what was printed:

for gamer in p1 | p2:
    invite_days = [d for d in [day1, day2] if d in gamer.availability]
    send_email(gamer, invite_days, "Abruptly Goblins!")

#prints
Dear Gamer(name='Barry Allen', availability=('Tuesday', 'Wednesday', 'Thursday')),

The Sorcery Society is happy to host "Abruptly Goblins!" night and wishes you will attend. Come by on ['Tuesday'] and have a blast!

Magically Yours,
The Sorcery Society

I tried changing some parameters but I couldn’t understand how to change the part after Dear, to Dear Barry Allen. I understand we have previously labeled, Gamer = namedtuple(‘Gamer’, [‘name’, ‘availability’]). Should I create another variable for the parameter? Can you help me with this part?

for gamer in p1 | p2:

I realised for this part, p1 | p2 is used as & to sends emails to both sets.
Using p1 and p2 only sends emails to Tuesday avail gamers.
Using p1, p2 is an invalid action.

Not sure what you mean by tuples and dicts, there’s not much of a relation between those things.

This value

Gamer(name='Barry Allen', availability=('Tuesday', 'Wednesday', 'Thursday')

Is a tuple, the first value (index 0) is the name, and the second value is a tuple of days. Those fields are also available as attributes:

a = Gamer(name='Barry Allen', availability=('Tuesday', 'Wednesday', 'Thursday')
print(a.name)
print(a[0])

(and yeah that’s definitely a bug in my send_email, should be name=attendee … it’s using a global variable that happens to have the right value so it “works”)

namedtuple is just a tuple with named fields

Not to both, but to the union of both. You could also write this as:

p1.union(p2)

& would be intersection (present in both)

>>> {1,2} & {2, 3}
{2}
>>> {1, 2} | {2, 3}
{1, 2, 3}

Ehh. Not sure what you mean, but, that information can’t be obtained by looking at p1 and p2 so, probably not. Anyway, not a great example to be looking at.
p1 is all that can come day1
p2 is all that can come day2 MINUS the ones in p1 because of earlier operations … this isn’t very useful to know, other than that p1 | p2 is the ones that we want to mail… I really feel like this is a horrible example because of earlier interactions making it both complicated and almost useless

and/or don’t do anything special for sets, they already have specific behaviours:

and: if left then right else left
or: if left then left else right

>>> bool([])
False
>>> bool([1])
True
>>> [1] and []
[]
>>> [] and [1]
[]
>>> [1] and [2]
[2]
>>> [1] or []
[1]
>>> [] or [1]
[1]
>>> [1] or [2]
[1]

|& and a whole lot else, including calling, indexing, iteration … are things that values control what they do:

class Thing:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f'Thing({self.name!r})'
    def __or__(self, other):
        print(f'__or__({self}, {other})')
    def __and__(self, other):
        print(f'__and__({self}, {other})')
    def __iter__(self):
        yield from 'kittens'

Thing('a') | Thing('b')
Thing('c') & Thing('d')

for thing in Thing('e'):
    print(thing)
__or__(Thing('a'), Thing('b'))
__and__(Thing('c'), Thing('d'))
k
i
t
t
e
n
s

but and/or operators aren’t one of them.

…you might not have meant those at all but there are a lot of and’s and or’s being talked about here and it’s not too clear what’s what.

Wow! When I first checked your reply, there was no edits, now there’s 6 additions. But Yes! I’ve managed to get the individual gamer names printed out.

For invited days appearing as [‘Tuesday’], I took your advice and thought about how to remove the [’ '] in the simplest way possible and I got it done! Maybe there’ll be a better way to refine it, but for now this is what I have.

for gamer in batch1 | batch2:
    invite_days = [day for day in [day1, day2] if day in gamer.availability]
    invite_days = invite_days[0]
    send_email(gamer.name, invite_days, "Abruptly Goblins!")
Dear Barry Allen,

The Sorcery Society is happy to host "Abruptly Goblins!" night and wishes you will attend. Come by on Tuesday and have a blast!

Magically Yours,
The Sorcery Society

I’m starting to see the sense of the union of batch1 and batch2 in your examples too.

For this part,
day1, batch1 = max(days.items(), key=lambda day:len(day[1]))

I just learnt the max() method requires 2 parameters (arg, key) to work that’s why they can’t be split up. So:
a) iterable = days.items() is a dictionary of day : gamers avail on day
b) key = definer of the factor max() needs to select on, i.e - key=lambda day:len(day[1] (a.k.a the day with the most gamers avail)

Also learnt that lambda is short for anonymous function, almost similar to def function.

For this:
day2, batch2 = max(((day, batch - batch1) for day, batch in days.items()), key=lambda day: len(day[1]))

This is max(arg1, key) right?
arg1 -> (day, batch - batch1) for day, batch in days.items())
key -> key=lambda day: len(day[1])

arg1 is where I’m positively lost. Does it mean:
(key, value - batch1) for key, value in dict.items(), where we are picking the remaining days, without the batch1 people as the iterable for our key to iterate?

Sorry if this is taking up a lot of your time to respond. I promise your effort is going to a good place with me. :slight_smile:

that’s me. post first write later.

You shouldn’t be trying to remove them, if you don’t want them then you shouldn’t have them in the first place, you should then instead be taking out the contents and making a string from that.
Taking just the first value defeats the purpose of a list doesn’t it.
Getting the grammar right with 1 or 2 days didn’t seem like any fun. A basic version would be to join the days with ", " between each one but that still makes for a confusing email.

send_email('sally', ['Tuesday'])
send_email('bob', ['Monday', 'Tuesday']) <- one email inviting for two days

it’s for in-lining functions, yeah. doesn’t do anything special other than that.

that’s what I need sets for, all for this day, except those that were in the first day
the reason why it’s a tuple is because the name of the day needs to be part of the result as well
peopleAvailableThisDay - peopleWhoCouldComeDay1

1 Like

I have no imagination.

Dear Querl Dox,

The Sorcery Society is happy to host "Abruptly Goblins" night and wishes you will attend.
Come by on any or all of the following days:
 - Friday
 - Tuesday

Magically Yours,
The Sorcery Society
def send_email(attendee, days, game):
    if len(days) == 0:
        raise ValueError('That\'s just rude.')
    if len(days) == 1:
        fdays = f'Come by on {days[0]} and have a blast!'
    else:
        when = '\n'.join(f' - {day}' for day in days)
        fdays = f'Come by on any or all of the following days:\n{when}'
    print(f'''\
Dear {attendee.name},

The Sorcery Society is happy to host "{game}" night and wishes you will attend.
{fdays}

Magically Yours,
The Sorcery Society
''')

This should be printed on a coffee mug.

2 Likes

Love this. :joy:

Thank you for the refining of the send_email function. It makes more sense. Cheers!