[Giveaway] Personal holiday game giveaway

:+1: Looks like itā€™s there now

5 Likes

I edited mine, itā€™s still good, right? (dreamArk)

3 Likes

Yep, all good. Theirs just didnā€™t go through originally

4 Likes

Wicked

Thank you for this giveaway, what an extensive list it really is.

Looking through the list reminds me of the huge number of games I obtained one way or another, mostly bundles that havenā€™t even been touched. Yet I go back to playing the same games with the limited time I have got online.

Cheers and happy holidays!

6 Likes

Meowski! Squeezes. Good to see thee. Hope your birdies and non-feathered family are well.

5 Likes

I got the keys and activated them.

Thanks for the giveaway and Happy Holidays, everyone! Have fun with your games! :grin:

4 Likes

Thanks for the games @murphysw !!

5 Likes

Thatā€™s a wrap folks. Iā€™m sent out DMā€™s to all 28 of you. If I accidentally skipped over you or you run into anything, please let me know. Iā€™m in the process of writing up a post mortem with all the details of how I did things and will post that here once itā€™s ready. Thanks all!

Also, I do still have 2 games left that went unclaimed if anyone wants them:
Zombie Driver HD
Fantasy Blacksmith

7 Likes

Post mortem

I wanted to give a breakdown of how I set this all up and give transparency into how the keys were divvied out. First off, Iā€™m a software engineer by trade and doing this was a fun little side project that I enjoyed putting together. Iā€™m going to provide all the code I used and instructions if anyone wants to tweak or reuse anything I did in the future or is just curious how it worked. Not sure if itā€™ll be worth the effort to do this for just a handful of keys, but it definitely was worth it for me not to have to hand pick and cross reference what people want and go back and forth forever until all 250 keys were gone. If anyone has a treasure trove theyā€™re looking to give away again, Iā€™d be happy to help.

In general, hereā€™s what I see as some of the benefits and gotchas to how I set things up.

Things I think went well

  • Allowed me to dole out 250+ keys at once without having to do a ton of manual effort (biggest piece was getting all the keys into an excel sheet, and then sending DM's out) that would have been a huge time sink and prone to errors
  • It leveled the playing field. The first people there didn't scoop up all or the best keys first (helps with time zones and sleep/work schedules) and people who didn't even see it for several days were still able to participate
  • It's hopefully fair. I tried to totally removed myself from any selection process (outside of fudging things a tiny bit at the end to ensure everyone got at least one key), selections were totally up to random chance, I set a threshold so that once people hit a number of winning keys, it skipped them if there were other people under the threshold who wanted it
  • Avoided spam by making the form require an email and making it so you could only submit one form (also made it editable which is nice)
  • I think I did everything safely. I didn't collect emails or anything and tried to verify that everyone who submitted was part of the group and who they said they were. The only info I was privy to was the list of choices that people made
  • Got some new faces to put in their first comment/post
  • It's hopefully somewhat reusable (given a new google sheet of keys that's already filled in, I could set up another one in less than 10 minutes

Things that could be better

  • The setup is pretty technical and does require more time investment than just dumping keys in a thread. While I enjoyed writing up the code and getting things all setup, it did take several hours to get it all together. If someone wanted to do it again, it still takes some knowhow to get things setup and working (although not nearly as time consuming now that the code is written)
  • Not sure it affected anyone, but it did require having a google account. Assuming everyone would already have one, but would be a pain to have to make a new account if not
  • The secret word thing probably wasn't necessary or could have been something better or more fun. I was just trying to avoid probably a scenario that would never happen where someone would submit two different forms with the same chrono user name and wouldn't know which was legit. My engineer brain is too dialed into trying to avoid any exploits some bad actor might try.
  • Had a couple different ideas of how to tweak how games were doled out. Ended up going with making things as up to chance as possible, but might have been better to have it pick winners in order from least wanted to most wanted games or to somehow rank or weight choices or something.
  • I tried to test everything best I could, but it's always possible I missed something in the code or process. Totally open to feedback/ideas/criticism on any of this. The code should be decent, but definitely isn't perfect or optimized (the google scripts IDE was pretty bare bones and I'm more used to C# and Typescript than base javascript)

Fun Fact

The top 10 most sought after games were:
  1. Sands of Aura
  2. Naruto to boruto
  3. Phoenix Point
  4. Strange Brigade
  5. BIT.TRIP.RUNNER
  6. Figment 2
  7. Flynn: Son of Crimson
  8. Ghostrunner
  9. Hyper Light Drifter
  10. Lego Marvel's Avengers

How I made this happen

First thing I had to do was to compile all my keys. This was a bit tedious, but I basically went through my humble bundle Keys & Entitlements page, my Fanatical product library, and my amazon prime free games page, (being careful to exclude any already redeemed/revealed keys and avoiding any that had a set time limit on them that had passed) and copied the name of the game and key into a new google sheet like so

From there, I setup a new google form. I manually added the header, description, and the first two questions, and a stub for the game list question without any values supplied.

I then wrote up (heavily borrowing from a tutorial online for the google integrations, see documentation here) a new Google Apps Script that essentially took the games and keys from a copy of my spreadsheet that removed all duplicates and added them as items into the third question (see code at bottom of post). The code could be improved to remove duplicates itself and to just use the master key list, but I didnā€™t got back and do that. I then finished setting up the form and published it. That then gave me the link to share in this post.

In the same way you can access the google sheets from the Apps script, you can also access the form response data as well. So from there I created a second script that pulls the response data and calculates the winners. Iā€™ll provide the code below with plenty of comments, but will also try to give a plain text version of what it does for those interested.

  1. It grabs the form and collects the responses so it can connect the user with the games they selected
  2. It grabs the spreadsheet with all the games and keys so it can connect the games with the keys (it's important for the game names to match the form response exactly here, there is a small quirk if you have a game that has both a steam and gog key that causes issues, so I had to just mark them both as steam to get by. Improvements could also be made to the script).
  3. It randomizes the order in which it's going to loop through the games (so that people hit the max game threshold in a random order, not alphabetically)
  4. It then loops through each game
    1. It pulls the list of users who requested the game (if there are none, it dumps them into a leftovers collection)>
    2. Then for each key (if there are multiple) it grabs a random person.
    3. If that person has not hit the max game threshold, it will add that key to that person's winning keys list and take them out of the running for more of the same game, then moves on to the next key/game
    4. If the user has hit the threshold, it will check to see if there are any other users who want the game that haven't hit the threshold and skip the user if there are so that it will pick the next random remaining person in the list
  5. At this point, we have a list of leftover keys, and a list of users and the games they won
  6. We spit the results out to a new Winners google sheet which includes the number of keys they got and the number of keys they requested (I used this to try and make things as fair as possible making sure that the people who requested a ton didn't end up with all the keys)
  7. The leftover keys are also spit out to a second sheet in that same document so they can be dealt with after without having to dig through the master sheet and know which ones were claimed or not

Now that we have the results sheet, I can just grab the list of games/keys for each user and can DM them with their list of keys (after throwing them at a JSON formatter).

What I actually did at this point was to run the script 5 times and find the one that looked like it distributed the keys the most evenly. I then may have also moved one key from one person with lots of keys to the one person who wasnā€™t going to get any to ensure that every person got at least one key.

Conclusion

In conclusion, Iā€™m a big nerd who had a ton of fun throwing this together and figuring out all the quirks while I had some time off work. Thank you all for putting trust in me to not waste all your time. It feels good to not let all these keys just go to waste and to be able to give them to people who have helped me keep track of getting free games over the last several years. I donā€™t post/comment often, but Iā€™m usually lurking in the main free game threads and always check out the new ones. If anyone has any questions for me or wants to go into more detail into anything, just let me know here or in a DM and Iā€™ll get back to you. Happy holidays all, and always remember to tweak your humble bundle donation sliders to make sure the charity actually gets some money.

ā€“ Spencer

CODE

Form game population

function openForm(e)
{
  populateQuestions();
}

function populateQuestions() {
  var form = FormApp.getActiveForm();
  var googleSheetsQuestions = getQuestionValues();
  var gameQuestion = form.getItems()[1];
  
  var choiceArray = [];
  for(j = 1; j < googleSheetsQuestions.length; j++)
  {
    (googleSheetsQuestions[j][0] != '') ? choiceArray.push(getDisplayName(googleSheetsQuestions[j])) : null;
  }
  gameQuestion.asCheckboxItem().setChoiceValues(choiceArray);
  
}

function getDisplayName(item) {
  var name = item[0];
  if (item[1] == "x"){
    name += " (GOG)"
  }
  return name;
}

function getQuestionValues() {
  var ss= SpreadsheetApp.openById('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
  var questionSheet = ss.getSheetByName('Games');
  var returnData = questionSheet.getDataRange().getValues();
  return returnData;
}

Calculate winners

const MAX_KEYS = 15;

function runMultiple() {
  for (let i = 1; i < 6; i++){
    assignWinners(i);
  }  
}

function assignWinners(numberOfRun) {
  if (!numberOfRun) {
    numberOfRun = 1;
  }
  // get the google form
  var form = FormApp.openById('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
  var usernameQuestion = form.getItems()[0];
  var gameChoiceQuestion = form.getItems()[2];
  var responses = form.getResponses();

  // get the google sheet with all the steam keys
  // it's currently structured with three columns with a header row (name, if game is from gog designated by an x, steam key) 
  var masterKeySheets = SpreadsheetApp.openById('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx').getSheets();
  var masterKeySheet = masterKeySheets[0];
  var gameObjects = masterKeySheet.getRange('A2:C264').getValues();
  // randomize the game order
  var randomGameOrder = shuffleArray(gameObjects);
  var leftoverGames = [];

  // create map of game name to keys
  var gameKeyMap = new Map();
  randomGameOrder.forEach((game) => {
    var gameName = getDisplayName(game);
    var keys = gameKeyMap.get(gameName) ?? [];
    keys.push(game[2]);
    gameKeyMap.set(gameName, keys);    
  });
  
  // create map of user to games they want
  var userToWantedGamesMap = new Map();
  responses.forEach((res) => {
      userToWantedGamesMap.set(res.getResponseForItem(usernameQuestion).getResponse(), res.getResponseForItem(gameChoiceQuestion).getResponse());
    });
  
  // create map of user to empty list (will populate with games won)
  var userToWinningGames = new Map();
  responses.forEach((res) => {
      userToWinningGames.set(res.getResponseForItem(usernameQuestion).getResponse(), []);
    });

  // iterate through keys by game
  gameKeyMap.forEach((gameKey, gameName) => {
    var usersWhoWantGame = [];
    // make a list of all users who want the game
    userToWantedGamesMap.forEach((wantedGames, user) => {
      if (wantedGames.includes(gameName)) {
        usersWhoWantGame.push(user);
      }
    }) 

    // for each game key (can be multiple)
    gameKey.forEach((key) => {
      if (usersWhoWantGame.length > 0) {
        var randomNumberPosition;
        // make an instanced copy of the list (so we can remove uses over their max, but still let them be eligible for a second key)
        var usersWhoWantGameInstance = usersWhoWantGame.slice();
        for (let i = 0; i < usersWhoWantGame.length; i++) {
          // chose a random person
          randomNumberPosition = Math.floor(Math.random()*usersWhoWantGameInstance.length);

          // get that user's current winning keys
          var currentKeys = userToWinningGames.get(usersWhoWantGameInstance[randomNumberPosition]);

          // if the user does not has the max number of keys already or if they are the only one left
          if (currentKeys.length < MAX_KEYS || i == usersWhoWantGame.length-1) {
            // Give that user the key
            currentKeys.push(`${gameName} - ${key}`);
            userToWinningGames.set(usersWhoWantGameInstance[randomNumberPosition], currentKeys);

            // remove user from the original list for that game so they don't get a second key
            usersWhoWantGame.splice(randomNumberPosition, 1);

            // break out of the for loop to go to the next game key
            break;
          }
          else {
            // remove user from the instanced list so we don't try to get them again in the random search (so that we go through all our max users before giving it to the last one)
            usersWhoWantGameInstance.splice(randomNumberPosition, 1);
          }
        }
      } else {
        // put all leftover keys in a separate place to know what's left
        leftoverGames.push([gameName, key]);
      }
    });
  });

  // add results to new spreadsheet
  var winnerSpreadSheet = SpreadsheetApp.create(`Winners-${numberOfRun}`);
  winnerSpreadSheet.getActiveSheet().setName('Winners'); 
  var winnerSheet = winnerSpreadSheet.getActiveSheet();
  userToWinningGames.forEach((winningGames, user) => {
    winnerSheet.appendRow([user, JSON.stringify(winningGames), winningGames.length, userToWantedGamesMap.get(user).length]);
  })

  var leftoverSheet = winnerSpreadSheet.insertSheet('Leftovers');
  leftoverGames.forEach((gameObj) => {
    leftoverSheet.appendRow(gameObj);
  }); 
  
}

function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
  return array;
}

function getDisplayName(item) {
  var name = item[0];
  if (item[1] == "x"){
    name += " (GOG)"
  }
  return name;
}
19 Likes

thatā€™s some awesome shit, legit

7 Likes

Thanks for doing a detailed post-mortem, honestly the process here was the most unique thing about this one.

7 Likes

Yeah, I admit: this is incredible!

And I really liked from this system. Way better than even a ā€œfirst come, first serveā€ type of giveaway, and also ninjas have no chance of swooping in and get keys for themselves.

Looking forward for more of this in the future. :+1:

7 Likes

I have an in person nerd who nom nom this code happily and have fun figuring out how to tweak it too, because he canā€™t leave well enough alone, XD.

This is pretty cool tbh. I donā€™t mess much with Java, but anything to make stuff easier. Even so, GAs do take hours to organize so your efforts are super appreciated. Cheers. :hibiscus:

6 Likes

This was a fascinating read. Really great documentation of your process! Thank you for sharing and thank you for the games! :star2:

5 Likes

Thanks again for doing this! And bonus points for describing everything! You put way more thought into it than I wouldā€™ve :rofl:

5 Likes

Thank you for the game keys , thatā€™s an ingenious way of making a giveaway i never seen such thing the effort you put in this wow thank you so much and happy holidays everyone

5 Likes

Wow, awesome!

I know nothing about coding and this is all foreign but still really fun to read about!

Also, thank you for the game!

5 Likes

That was probably the biggest giveaway in Chronoā€™s history! Thanks again, you improved my Christmas haha

6 Likes

Thank you more than I can express. Wow, that is a bunch of games. May your holidays bring you as much joy as you are bringing to all of us.

4 Likes

I liked the secret word part. Itā€™s fun to see what people choose.

Also, this whole system of yours was cool.

6 Likes