Scala 9146 (min 7, max 15, moyenne 9.15) durée: 2000 secondes
Comme beaucoup d'entrées, je commence par obtenir la longueur totale, puis les espaces, un peu plus d'informations, la réduction aux candidats restants et des devinettes.
Inspiré par la bande dessinée originale xkcd, j'ai essayé d'appliquer ma compréhension rudimentaire de la théorie de l'information. Il y a un billion de phrases possibles ou un peu moins de 40 bits d'entropie. Je me suis fixé comme objectif moins de 10 suppositions par phrase de test, ce qui signifie que nous devons apprendre en moyenne près de 5 bits par requête (car la dernière est inutile). À chaque conjecture, nous obtenons deux nombres et, grosso modo, plus l'éventail de ces nombres est grand, plus nous nous attendons à en apprendre.
Pour simplifier la logique, j’utilise chaque requête comme deux questions distinctes. Chaque chaîne de devinettes est donc composée de deux parties: un côté gauche s'intéresse au nombre de positions correctes (chevilles noires dans le cerveau) et un côté droit s’intéresse au nombre de caractères corrects ( total des piquets). Voici un jeu typique:
Phrase: chasteness legume such
1: p0 ( 1/21) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -aaaaaaaaaaaabbbbbbbbbcccccccccdddddddddeeeeeeeeeeeeeeefffffffffgggggggggggghhhhhhhhhiiiiiiiiiiiiiiiiiijjjjjjkkkkkkkkkllllllllllllmmmmmmmmmnnnnnnnnnnnnoooooooooooopppppppppqqqrrrrrrrrrrrrssssssssssssssstttttttttuuuuuuuuuuuuvvvvvvwwwwwwxxxyyyyyyyyyzzzzzz
2: p1 ( 0/ 8) - - - --- - ---aaaaaaaaaaaadddddddddeeeeeeeeeeeeeeefffffffffjjjjjjkkkkkkkkkllllllllllllooooooooooooqqqwwwwwwxxxyyyyyyyyyzzzzzz
3: p1 ( 0/11) ----- ------ ---------bbbbbbbbbdddddddddeeeeeeeeeeeeeeefffffffffgggggggggggghhhhhhhhhiiiiiiiiiiiiiiiiiikkkkkkkkkllllllllllllppppppppptttttttttvvvvvv
4: p1 ( 2/14) ---------- ------ ----ccccccccceeeeeeeeeeeeeeehhhhhhhhhkkkkkkkkkllllllllllllmmmmmmmmmooooooooooooqqqrrrrrrrrrrrrsssssssssssssssvvvvvvwwwwwwzzzzzz
5: p3 ( 3/ 3) iaaiiaaiai iaaiia iaaiaaaaaaaaaaaabbbbbbbbbdddddddddiiiiiiiiiiiiiiiiiikkkkkkkkkllllllllllllqqquuuuuuuuuuuuvvvvvvyyyyyyyyy
6: p3 ( 3/11) aaaasassaa aaaasa aaaaaaaaaaaaaaaabbbbbbbbbcccccccccdddddddddfffffffffhhhhhhhhhppppppppprrrrrrrrrrrrssssssssssssssstttttttttuuuuuuuuuuuuwwwwwwxxxyyyyyyyyy
7: p4 ( 4/10) accretions shrive pews
8: p4 ( 4/ 6) barometric terror heir
9: p4 SUCCESS chasteness legume such
Espaces de devinettes
Chaque estimation d'espace peut renvoyer au plus 2 piquets noirs; J'ai essayé de construire des hypothèses pour renvoyer 0,1 et 2 chevilles avec des probabilités 1 / 4,1 / 2 et 1/4 respectivement. Je pense que c’est le mieux que vous puissiez faire pour un volume d’information attendu de 1,5 bit. J'ai opté pour une chaîne en alternance pour la première estimation, suivie de celles générées aléatoirement, bien qu'il soit généralement intéressant de commencer à deviner dès la deuxième ou la troisième tentative, car nous connaissons les fréquences de longueur des mots.
L'apprentissage des jeux de caractères compte
Pour les suppositions du côté droit, je choisis des ensembles de caractères aléatoires (toujours 2 de e / i / a / s) afin que le nombre attendu renvoyé soit égal à la moitié de la longueur de la phrase. Une variance plus élevée signifie plus d'informations et d'après la page wikipedia sur la distribution binomiale, je calcule environ 3,5 bits par requête (au moins pour les premiers avant que les informations ne deviennent redondantes). Une fois l'espacement connu, j'utilise des chaînes aléatoires des lettres les plus courantes sur le côté gauche, choisies de manière à ne pas entrer en conflit avec le côté droit.
Coalescence des candidats restants
Ce jeu est un compromis entre vitesse de traitement / efficacité de la requête et l'énumération des candidats restants peut prendre très longtemps sans informations structurées telles que des caractères spécifiques. J'ai optimisé cette partie en recueillant principalement des informations invariantes dans l'ordre des mots, ce qui me permet de pré-calculer les comptes de jeux de caractères pour chaque mot et de les comparer avec les comptes tirés des requêtes. Je compresse ces nombres dans un entier long, en utilisant le comparateur d'égalité de la machine et l'additionneur pour tester tous mes nombres de caractères en parallèle. C'était une victoire énorme. Je peux emballer jusqu'à 9 chefs d'accusation dans le Long, mais j'ai trouvé que la collecte des informations supplémentaires n'en valait pas la peine et j'ai opté pour 6 à 7.
Une fois que les candidats restants sont connus, si l'ensemble est raisonnablement petit, je sélectionne celui dont le journal prévu est le plus faible des candidats restants. Si le jeu est assez grand pour que cela prenne beaucoup de temps, je choisis un petit jeu d'échantillons.
Merci tout le monde. C'était un jeu amusant et m'a incité à m'inscrire sur le site.
Mise à jour: code épuré pour plus de simplicité et de lisibilité, avec des ajustements mineurs à l’algorithme, ce qui a permis d’améliorer le score.
Score original: 9447 (min 7, max 13, moyenne 9,45) temps: 1876 secondes
Le nouveau code est 278 lignes de Scala, ci-dessous
object HorseBatteryStapleMastermind {
def main(args: Array[String]): Unit = run() print ()
val n = 1000 // # phrases to run
val verbose = true // whether to print each game
//tweakable parameters
val prob = 0.132 // probability threshold to guess spacing
val rngSeed = 11 // seed for random number generator
val minCounts = 6 // minimum char-set counts before guessing
val startTime = System.currentTimeMillis()
def time = System.currentTimeMillis() - startTime
val phraseList = io.Source.fromFile("pass.txt").getLines.toArray
val wordList = io.Source.fromFile("words.txt").getLines.toArray
case class Result(num: Int = 0, total: Int = 0, min: Int = Int.MaxValue, max: Int = 0) {
def update(count: Int) = Result(num + 1, total + count, Math.min(count, min), Math.max(count, max))
def resultString = f"#$num%4d Total: $total%5d Avg: ${total * 1.0 / num}%2.2f Range: ($min%2d-$max%2d)"
def timingString = f"Time: Total: ${time / 1000}%5ds Avg: ${time / (1000.0 * num)}%2.2fs"
def print() = println(s"$resultString\n$timingString")
}
def run(indices: Set[Int] = (0 until n).to[Set], prev: Result = Result()): Result = {
if (verbose && indices.size < n) prev.print()
val result = prev.update(Querent play Oracle(indices.head, phraseList(indices.head)))
if (indices.size == 1) result else run(indices.tail, result)
}
case class Oracle(idx: Int, phrase: String) {
def query(guess: String) = Grade.compute(guess, phrase)
}
object Querent {
def play(oracle: Oracle, n: Int = 0, notes: Notes = Notes0): Int = {
if (verbose && n == 0) println("=" * 100 + f"\nPhrase ${oracle.idx}%3d: ${oracle.phrase}")
val guess = notes.bestGuess
val grade = oracle.query(guess)
if (verbose) println(f"${n + 1}%2d: p${notes.phase} $grade $guess")
if (grade.success) n + 1 else play(oracle, n + 1, notes.update(guess, grade))
}
abstract class Notes(val phase: Int) {
def bestGuess: String
def update(guess: String, grade: Grade): Notes
}
case object Notes0 extends Notes(0) {
def bestGuess = GuessPack.firstGuess
def genSpaceCandidates(grade: Grade): List[Spacing] = (for {
wlen1 <- WordList.lengthRange
wlen2 <- WordList.lengthRange
spacing = Spacing(wlen1, wlen2, grade.total)
if spacing.freq > 0
if grade.black == spacing.black(bestGuess)
} yield spacing).sortBy(-_.freq).toList
def update(guess: String, grade: Grade) =
Notes1(grade.total, genSpaceCandidates(grade), Limiter(Counts.withMax(grade.total - 2), Nil), GuessPack.stream)
}
case class Notes1(phraseLength: Int, spacingCandidates: List[Spacing], limiter: Limiter, guesses: Stream[GuessPack]) extends Notes(1) {
def bestGuess = (chance match {
case x if x < prob => guesses.head.spacing.take(phraseLength)
case _ => spacingCandidates.head.mkString
}) + guesses.head.charSet
def totalFreq = spacingCandidates.foldLeft(0l)({ _ + _.freq })
def chance = spacingCandidates.head.freq * 1.0 / totalFreq
def update(guess: String, grade: Grade) = {
val newLim = limiter.update(guess, grade)
val newCands = spacingCandidates.filter(_.black(guess) == grade.black)
newCands match {
case best :: Nil if newLim.full => Notes3(newLim.allCandidates(best))
case best :: Nil => Notes2(best, newLim, guesses.tail)
case _ => Notes1(phraseLength, newCands, newLim, guesses.tail)
}
}
}
case class Notes2(spacing: Spacing, limiter: Limiter, guesses: Stream[GuessPack]) extends Notes(2) {
def bestGuess = tile(guesses.head.pattern) + guesses.head.charSet
def whiteSide(guess: String): String = guess.drop(spacing.phraseLength)
def blackSide(guess: String): String = guess.take(spacing.phraseLength)
def tile(guess: String) = spacing.lengths.map(guess.take).mkString(" ")
def untile(guess: String) = blackSide(guess).split(" ").maxBy(_.length) + "-"
def update(guess: String, grade: Grade) = {
val newLim = limiter.updateBoth(whiteSide(guess), untile(guess), grade)
if (newLim.full)
Notes3(newLim.allCandidates(spacing))
else
Notes2(spacing, newLim, guesses.tail)
}
}
case class Notes3(candidates: Array[String]) extends Notes(3) {
def bestGuess = sample.minBy(expLogNRC)
def update(guess: String, grade: Grade) =
Notes3(candidates.filter(phrase => grade == Grade.compute(guess, phrase)))
def numRemCands(phrase: String, guess: String): Int = {
val grade = Grade.compute(guess, phrase)
sample.count(phrase => grade == Grade.compute(guess, phrase))
}
val sample = if (candidates.size <= 32) candidates else candidates.sortBy(_.hashCode).take(32)
def expLogNRC(guess: String): Double = sample.map(phrase => Math.log(1.0 * numRemCands(phrase, guess))).sum
}
case class Spacing(wl1: Int, wl2: Int, phraseLength: Int) {
def wl3 = phraseLength - 2 - wl1 - wl2
def lengths = Array(wl1, wl2, wl3)
def pos = Array(wl1, wl1 + 1 + wl2)
def freq = lengths.map(WordList.freq).product
def black(guess: String) = pos.count(guess(_) == ' ')
def mkString = lengths.map("-" * _).mkString(" ")
}
case class Limiter(counts: Counts, guesses: List[String], extraGuesses: List[(String, Grade)] = Nil) {
def full = guesses.size >= minCounts
def update(guess: String, grade: Grade) =
if (guesses.size < Counts.Max)
Limiter(counts.update(grade.total - 2), guess :: guesses)
else
Limiter(counts, guesses, (guess, grade) :: extraGuesses)
def updateBoth(whiteSide: String, blackSide: String, grade: Grade) =
Limiter(counts.update(grade.total - 2).update(grade.black - 2), blackSide :: whiteSide :: guesses)
def isCandidate(phrase: String): Boolean = extraGuesses forall {
case (guess, grade) => grade == Grade.compute(guess, phrase)
}
def allCandidates(spacing: Spacing): Array[String] = {
val order = Array(0, 1, 2).sortBy(-spacing.lengths(_)) //longest word first
val unsort = Array.tabulate(3)(i => order.indexWhere(i == _))
val wordListI = WordList.byLength(spacing.lengths(order(0)))
val wordListJ = WordList.byLength(spacing.lengths(order(1)))
val wordListK = WordList.byLength(spacing.lengths(order(2)))
val gsr = guesses.reverse
val countsI = wordListI.map(Counts.compute(_, gsr).z)
val countsJ = wordListJ.map(Counts.compute(_, gsr).z)
val countsK = wordListK.map(Counts.compute(_, gsr).z)
val rangeI = 0 until wordListI.size
val rangeJ = 0 until wordListJ.size
val rangeK = 0 until wordListK.size
(for {
i <- rangeI.par
if Counts(countsI(i)) <= counts
j <- rangeJ
countsIJ = countsI(i) + countsJ(j)
if Counts(countsIJ) <= counts
k <- rangeK
if countsIJ + countsK(k) == counts.z
words = Array(wordListI(i), wordListJ(j), wordListK(k))
phrase = unsort.map(words).mkString(" ")
if isCandidate(phrase)
} yield phrase).seq.toArray
}
}
object Counts {
val Max = 9
val range = 0 until Max
def withMax(size: Int): Counts = Counts(range.foldLeft(size.toLong) { (z, i) => (z << 6) | size })
def compute(word: String, x: List[String]): Counts = x.foldLeft(Counts.withMax(word.length)) { (c: Counts, s: String) =>
c.update(if (s.last == '-') Grade.computeBlack(word, s) else Grade.computeTotal(word, s))
}
}
case class Counts(z: Long) extends AnyVal {
@inline def +(that: Counts): Counts = Counts(z + that.z)
@inline def apply(i: Int): Int = ((z >> (6 * i)) & 0x3f).toInt
@inline def size: Int = this(Counts.Max)
def <=(that: Counts): Boolean =
Counts.range.forall { i => (this(i) <= that(i)) && (this.size - this(i) <= that.size - that(i)) }
def update(c: Int): Counts = Counts((z << 6) | c)
override def toString = Counts.range.map(apply).map(x => f"$x%2d").mkString(f"Counts[$size%2d](", " ", ")")
}
case class GuessPack(spacing: String, charSet: String, pattern: String)
object GuessPack {
util.Random.setSeed(rngSeed)
val RBF: Any => Boolean = _ => util.Random.nextBoolean() //Random Boolean Function
def genCharsGuess(q: Char => Boolean): String =
(for (c <- 'a' to 'z' if q(c); j <- 1 to WordList.maxCount(c)) yield c).mkString
def charChooser(i: Int)(c: Char): Boolean = c match {
case 'e' => Array(true, true, true, false, false, false)(i % 6)
case 'i' => Array(false, true, false, true, false, true)(i % 6)
case 'a' => Array(true, false, false, true, true, false)(i % 6)
case 's' => Array(false, false, true, false, true, true)(i % 6)
case any => RBF(any)
}
def genSpaceGuess(q: Int => Boolean = RBF): String = genPatternGuess(" -", q)
def genPatternGuess(ab: String, q: Int => Boolean = RBF) =
(for (i <- 0 to 64) yield (if (q(i)) ab(0) else ab(1))).mkString
val firstGuess = genSpaceGuess(i => (i % 2) == 1) + genCharsGuess(_ => true)
val stream: Stream[GuessPack] = Stream.from(0).map { i =>
GuessPack(genSpaceGuess(), genCharsGuess(charChooser(i)), genPatternGuess("eias".filter(charChooser(i))))
}
}
}
object WordList {
val lengthRange = wordList.map(_.length).min until wordList.map(_.length).max
val byLength = Array.tabulate(lengthRange.end)(i => wordList.filter(_.length == i))
def freq(wordLength: Int): Long = if (lengthRange contains wordLength) byLength(wordLength).size else 0
val maxCount: Map[Char, Int] = ('a' to 'z').map(c => (c -> wordList.map(_.count(_ == c)).max * 3)).toMap
}
object Grade {
def apply(black: Int, white: Int): Grade = Grade(black | (white << 8))
val Success = Grade(-1)
def computeBlack(guess: String, phrase: String): Int = {
@inline def posRange: Range = 0 until Math.min(guess.length, phrase.length)
@inline def sameChar(p: Int): Boolean = (guess(p) == phrase(p)) && guess(p) != '-'
posRange count sameChar
}
def computeTotal(guess: String, phrase: String): Int = {
@inline def minCount(c: Char): Int = Math.min(phrase.count(_ == c), guess.count(_ == c))
minCount(' ') + ('a' to 'z').map(minCount).sum
}
def compute(guess: String, phrase: String): Grade = {
val black = computeBlack(guess, phrase)
if (black == guess.length && black == phrase.length)
Grade.Success
else
Grade(black, computeTotal(guess, phrase) - black)
}
}
case class Grade(z: Int) extends AnyVal {
def black: Int = z & 0xff
def white: Int = z >> 8
def total: Int = black + white
def success: Boolean = this == Grade.Success
override def toString = if (success) "SUCCESS" else f"($black%2d/$white%2d)"
}
}