Skip to content

Instantly share code, notes, and snippets.

@shanecelis
Last active December 3, 2020 07:40
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 shanecelis/b88808f5198832dd5f3dd2015017f0ec to your computer and use it in GitHub Desktop.
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.
/* 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