Last active
December 3, 2020 07:40
-
-
Save shanecelis/b88808f5198832dd5f3dd2015017f0ec to your computer and use it in GitHub Desktop.
Solves a SecretSanta constraint problem using CatSAT; works in Unity because I'm lazy.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Original code Copyright (c) 2018 Shane Celis[1] | |
Licensed under the MIT License[2] | |
Original code posted here[3]. | |
This comment generated by code-cite[4]. | |
[1]: https://twitter.com/shanecelis | |
[2]: https://opensource.org/licenses/MIT | |
[3]: https://gist.github.com/shanecelis/b88808f5198832dd5f3dd2015017f0ec | |
[4]: https://github.com/shanecelis/code-cite | |
*/ | |
// #define TEST_EMAIL | |
using System; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using CatSAT; | |
using static CatSAT.Language; | |
using Debug = UnityEngine.Debug; | |
[System.Serializable] | |
public struct Person { | |
public string name; | |
public string email; | |
} | |
[System.Serializable] | |
public struct Pair { | |
public string secretSanta; | |
public string santaee; | |
} | |
[System.Serializable] | |
public struct NamedPairs { | |
public string name; | |
public bool enable; | |
public List<Pair> pairs; | |
} | |
/** | |
Here's a little Secret Santa emailer, so you can be as surprised as everyone | |
else. | |
*/ | |
public class SecretSanta : MonoBehaviour { | |
public string name; // Usually the year. | |
public List<Person> people; | |
public List<Pair> oneWayWhiteList; | |
public List<Pair> oneWayBlackList; | |
public List<Pair> twoWayBlackList; | |
public List<NamedPairs> history; | |
public bool sendEmails = false; | |
public List<Pair> pairs; | |
void Start() { | |
#if TEST_EMAIL | |
Mail("shane.celis@gmail.com", "test script", "yo 0"); | |
#else | |
SolveSecretSanta(); | |
#endif | |
} | |
void SolveSecretSanta() { | |
CatSAT.Random.SetSeed(); | |
var p = new Problem("Secret Santa List"); | |
var email = new Dictionary<string, string>(); | |
foreach(var person in people) | |
email[person.name] = person.email; | |
var _secretSanta = Predicate<string, string>("secretSanta"); | |
Func<string, string, Proposition> secretSanta = (string secretSantaName, string santaeeName) => { | |
if (! email.ContainsKey(secretSantaName)) | |
throw new Exception($"No secret santa email for {secretSantaName}"); | |
if (! email.ContainsKey(santaeeName)) | |
throw new Exception($"No secret santaee email for {santaeeName}"); | |
return _secretSanta(secretSantaName, santaeeName); | |
}; | |
var couple = Predicate<string, string>("couple"); | |
var names = people.Select(z => z.name).ToList(); | |
foreach(var x in names) { | |
p.Assert(// You can't be your own secret santa. | |
Not(secretSanta(x, x))); | |
foreach (var y in names) { | |
p.Assert(// No cycles. | |
secretSanta(x, y) > Not(secretSanta(y, x))); | |
} | |
// You can only be secret santa to one person. | |
p.Exactly(1, names, y => secretSanta(x, y)); | |
// Only one person can be secret santa to you. | |
p.Exactly(1, names, y => secretSanta(y, x)); | |
} | |
foreach (var x in names) { | |
Debug.Log($"{x} was Secret Santa for " + | |
string.Join(", ", LastSantaees(x, history) | |
.OrderByDescending(t => int.Parse(t.Item2)) | |
.Select(t => $"{t.Item1} ({t.Item2})"))); | |
Debug.Log($"{x} had these Secret Santas: " + | |
string.Join(", ", LastSecretSantas(x, history) | |
.OrderByDescending(t => int.Parse(t.Item2)) | |
.Select(t => $"{t.Item1} ({t.Item2})"))); | |
} | |
// foreach(var pair in lastPairs) { | |
// p.Assert(Not(secretSanta(pair.secretSanta, pair.santaee))); | |
// } | |
// | |
// foreach(var pair in lastPairs2) { | |
// p.Assert(Not(secretSanta(pair.secretSanta, pair.santaee))); | |
// } | |
// Exclude the secret santa pairings from last time. | |
foreach (var h in history) { | |
if (! h.enable) { | |
Debug.Log($"History {h.name} not enabled; skipping."); | |
continue; | |
} | |
foreach (var pair in h.pairs) { | |
// if (! email.ContainsKey(pair.secretSanta)) | |
// throw new Exception($"No secret santa email for {pair.secretSanta}"); | |
// | |
// if (! email.ContainsKey(pair.santaee)) | |
// throw new Exception($"No secret santa email for {pair.santaee}"); | |
p.Assert(Not(secretSanta(pair.secretSanta, pair.santaee))); | |
} | |
} | |
// If there are matches that must be made. | |
foreach(var pair in oneWayWhiteList) { | |
p.Assert(secretSanta(pair.secretSanta, pair.santaee)); | |
} | |
// If there are any one way exclusions. | |
foreach(var pair in oneWayBlackList) { | |
p.Assert(Not(secretSanta(pair.secretSanta, pair.santaee))); | |
} | |
// If there are any two way exclusions. | |
foreach(var pair in twoWayBlackList) { | |
p.Assert(Not(secretSanta(pair.secretSanta, pair.santaee)), | |
Not(secretSanta(pair.santaee, pair.secretSanta))); | |
} | |
var s = p.Solve(true); | |
pairs.Clear(); | |
var mailings = new List<Action>(); | |
foreach(var x in names) { | |
foreach (var y in names) { | |
if (s[secretSanta(x, y)]) { | |
if (! sendEmails) { | |
// If you're not sending emails, you're just testing. | |
Debug.Log($"{x} is the secret santa for {y}."); | |
} else { | |
// Don't spoil yourself. | |
Debug.Log("X is the secret santa for Y."); | |
} | |
pairs.Add(new Pair { secretSanta = x, santaee = y }); | |
// Don't do it immediately in case there's an error. | |
mailings.Add(() => Mail(email[x], $"Secret Santa {name}: Keep it secret! Keep it safe!", | |
$"{x}, you are the Secret Santa for {y}.\n" | |
+ (LastSantaees(x, history).Any() ? | |
$"\nYou were Secret Santa for " + | |
string.Join(", ", LastSantaees(x, history) | |
.OrderByDescending(t => int.Parse(t.Item2)) | |
.Select(t => $"{t.Item1} ({t.Item2})")) | |
+ ".\n" | |
: "") | |
+ | |
(LastSecretSantas(x, history).Any() | |
? $"\nYou had these Secret Santas in Christmases past: " + | |
string.Join(", ", LastSecretSantas(x, history) | |
.OrderByDescending(t => int.Parse(t.Item2)) | |
.Select(t => $"{t.Item1} ({t.Item2})")) + ".\n" | |
: "") | |
+ "\n" | |
+ "* * *\n" | |
+ "\n" | |
+ "Brought to you by SecretSanta.cs[1].\n" | |
+ "\n" | |
+ "[1]: https://gist.github.com/shanecelis/b88808f5198832dd5f3dd2015017f0ec\n")); | |
} | |
} | |
} | |
bool foundProblem = false; | |
foreach (var pair in pairs) { | |
foreach (var h in history) { | |
if (Contains(pair, h.pairs)) { | |
Debug.LogWarning($"Pair {pair} happened in {h.name}."); | |
foundProblem = true; | |
} | |
} | |
} | |
if (foundProblem) | |
throw new Exception("A pair happened before"); | |
// No error. Mail 'em. | |
foreach(var mail in mailings) | |
mail(); | |
if (sendEmails) | |
Debug.Log($"Sent {mailings.Count} emails."); | |
else | |
Debug.Log($"Would have sent {mailings.Count} emails."); | |
} | |
/** | |
This is going to be the hardest part to actually make work. Getting your | |
mailer to actually relay things through, say, smtp.gmail.com is the worst. | |
*/ | |
void Mail(string email, string subject, string body) { | |
if (sendEmails | |
#if TEST_EMAIL | |
|| true | |
#endif | |
) { | |
Exec("mail", $"-s \"{subject}\" {email}", body); | |
} else { | |
Debug.Log($"echo {body} | mail -s \"{subject}\" {email}"); | |
} | |
} | |
// https://stackoverflow.com/questions/4788863/how-to-send-series-of-commands-to-a-command-window-process | |
private string Exec(string cmd, string args, string input) { | |
var proc = new ProcessStartInfo(cmd); | |
proc.Arguments = args; | |
proc.RedirectStandardInput = true; | |
proc.RedirectStandardOutput = true; | |
proc.UseShellExecute = false; | |
Process process = Process.Start(proc); | |
if (process != null) { | |
process.StandardInput.Write(input); | |
process.StandardInput.Close(); | |
string outputString = process.StandardOutput.ReadToEnd(); | |
return outputString; | |
} | |
return string.Empty; | |
} | |
/** Return the santaees for this person. */ | |
private static IEnumerable<(string, string)> LastSantaees(string secretSanta, List<NamedPairs> namedPairs) { | |
return namedPairs.SelectMany(np => | |
np.pairs | |
.Where(p => p.secretSanta == secretSanta) | |
.Select(p=> (p.santaee, np.name))); | |
} | |
/** Return the secret santas for this person. */ | |
private static IEnumerable<(string, string)> LastSecretSantas(string santaee, List<NamedPairs> namedPairs) { | |
return namedPairs.SelectMany(np => | |
np.pairs | |
.Where(p => p.santaee == santaee) | |
.Select(p=> (p.secretSanta, np.name))); | |
} | |
private static bool Contains(Pair pair, List<Pair> pairs) { | |
foreach (var p in pairs) { | |
if (p.secretSanta == pair.secretSanta && | |
p.santaee == pair.santaee) | |
return true; | |
} | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment