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)"
  }
}