Potion Bottle Icon Manuel d'alchimie du code Potion Bottle Icon

Puzzles libres en Scala — Atelier LINUQ

- 415 mots - Temps de lecture estimé: 2 minutes

J’ai eu la chance de présenter un atelier sur les puzzles libres en Scala au LINUQ. L’idée : utiliser la programmation par contraintes pour résoudre des casse-têtes classiques, le tout en code source libre. Voici le contenu de l’atelier.


Sun Face IconC’est quoi un puzzle libre en Scala ?Sun Face Icon


Un atelier pratique qui combine programmation par contraintes, langage Scala et bibliothèque OscaR pour résoudre des énigmes comme le Sudoku, les N-Reines et les mots croisés, avec du code source libre.

🌘 Contexte

Le LINUQ (Linux UQ) est le groupe d’utilisateurs Linux et de logiciels libres de l’Université du Québec. Dans le cadre de leurs ateliers, j’ai proposé une séance de programmation par contraintes en Scala.

La programmation par contraintes est un paradigme où tu déclares les relations (contraintes) qui doivent être satisfaites, et le moteur de résolution cherche une solution qui les respecte toutes. C’est particulièrement adapté aux puzzles logiques.

graph TD
    A[Déclarer les variables CPIntVar] --> B[Définir leur domaine de valeurs]
    B --> C[Ajouter les contraintes<br>ex: allDifferent]
    C --> D[Lancer la recherche<br>search(binaryFirstFail)]
    D --> E{Solution trouvée?}
    E -->|Oui| F[Afficher avec onSolution]
    E -->|Non| G[Backtrack automatique]
    F --> D
    G --> D
    D --> H[Aucune solution? → stats]

La bibliothèque utilisée est OscaR, une librairie Scala open source pour la recherche opérationnelle.

🌘 Le projet SBT

Le projet est structuré avec SBT (Scala Build Tool) 1.1.1. Une seule dépendance externe : OscaR 3.1.0.

// build.sbt
name := "puzzles_libre"
version := "0.1"
scalaVersion := "2.11.12"

resolvers += "Artifactory" at "http://artifactory.info.ucl.ac.be/artifactory/libs-release/"

libraryDependencies += "oscar" % "oscar-cp_2.11" % "3.1.0"
// project/build.properties
sbt.version = 1.1.1

Pour lancer un puzzle :

sbt "runMain Sudoku2"
sbt "runMain Queens"
sbt "runMain CrossWord"

🌘 Les puzzles

🌘 Sudoku

Le solveur de Sudoku utilise OscaR avec des contraintes allDifferent sur les lignes, colonnes et régions. Trois contraintes suffisent pour résoudre la grille :

graph TD
    subgraph "Contraintes du Sudoku"
        L[add(allDifferent⁡(x(i)))] --> V[Valeurs uniques par ligne]
        C[add(allDifferent⁡(x_t(i)))] --> V
        R[add(allDifferent⁡(région 3×3))] --> V
    end
    V --> S[Solution complète]
import oscar.cp._
import scala.io.Source._
import scala.math._

object Sudoku2 extends CPModel with App {

  var n = 9
  var reg = 3
  val X = 0

  var problem = Array(
    Array(X, X, X, 2, X, 5, X, X, X),
    Array(X, 9, X, X, X, X, 7, 3, X),
    Array(X, X, 2, X, X, 9, X, 6, X),
    Array(2, X, X, X, X, X, 4, X, 9),
    Array(X, X, X, X, 7, X, X, X, X),
    Array(6, X, 9, X, X, X, X, X, 1),
    Array(X, 8, X, 4, X, X, 1, X, X),
    Array(X, 6, 3, 1, X, X, X, 8, X),
    Array(X, X, X, 6, X, 8, X, X, X))

  val alpha = ('0' to '9') ++ ('A' to 'Z')

  if (args.length > 0) {
    val fileName = args(0)
    println("\nReading from file: " + fileName)
    val lines = fromFile(fileName).getLines.filter(!_.startsWith("#")).toArray
    n = lines(0).toInt
    reg = sqrt(n).toInt
    println("Size:" + n)
    val this_problem = lines.tail.map(_.split("\\s+").
      filter(_.length>0).map(i=>if (i == ".") X else i.toInt))
    problem = this_problem
  }

  val x = Array.fill(n,n)(CPIntVar(1 to n))
  val x_t = x.transpose

  for(i <- NRANGE; j <- NRANGE if problem(i)(j) > 0) {
    add(x(i)(j) == problem(i)(j))
  }

  for(i <- NRANGE) {
    add(allDifferent(x(i)), Strong)
    add(allDifferent(x_t(i)), Strong)
  }

  for(i <- RRANGE; j <- RRANGE) {
    add(allDifferent((for{ r <- i*reg until i*reg+reg;
                           c <- j*reg until j*reg+reg
    } yield x(r)(c))), Strong)
  }

  search { binaryFirstFail(x.flatten.toSeq) }

  onSolution {
    println("\nSolution:")
    for(i <- 0 until n) {
      println(x(i).map(j=>alpha(j.value)).mkString(" "))
    }
    numSols += 1
  }

  val stats = start()
  println(stats)
}

🌘 N-Reines

Le problème classique des N-Reines : placer 12 reines sur un échiquier sans qu’aucune ne se menace. La solution tient en quelques lignes grâce aux contraintes allDifferent. Chaque reine est représentée par sa colonne, et trois contraintes assurent qu’aucune ne partage la même ligne, diagonale montante ou descendante :

graph LR
    subgraph "12 reines sur 12×12"
        Q[queens = Array(12 CPIntVar)]
    end
    Q --> C1[allDifferent⁡(queens)]
    Q --> C2[allDifferent⁡(queens + i)]
    Q --> C3[allDifferent⁡(queens - i)]
    C1 --> R1[Aucune reine sur la même ligne]
    C2 --> R2[Aucune sur la diagonale ↗]
    C3 --> R3[Aucune sur la diagonale ↘]
import oscar.cp._
object Queens extends CPModel with App {
  val nQueens = 12
  val Queens = 0 until nQueens
  val queens = Array.fill(nQueens)(CPIntVar.sparse(0, nQueens - 1))

  add(allDifferent(queens))
  add(allDifferent(Queens.map(i => queens(i) + i)))
  add(allDifferent(Queens.map(i => queens(i) - i)))

  search(binaryFirstFail(queens))

  onSolution {
    print("Solution potentielle\n")
    println(queens.mkString(" "))
  }

  val stats = start()
  println(stats)
}

🌘 Mots croisés

Le plus complexe des trois : un puzzle de mots croisés où les mots doivent s’entrecroiser correctement. Les contraintes d’intersection sont modélisées via une matrice de chevauchements. La variable E sélectionne quels mots parmi les 15 candidats sont placés horizontalement, et les contraintes d’intersection lient leurs lettres aux mots verticaux :

graph LR
    subgraph "Mots horizontaux (candidats)"
        H0[0: HOSES]
        H3[3: SAILS]
        H6[6: HEEL]
        H7[7: HIKE]
    end
    subgraph "Mots verticaux (candidats)"
        V1[1: LASER]
        V2[2: SAILS]
        V4[4: SHEET]
        V5[5: STEER]
    end
    H0 -- "E(0)∩E(1)" --> V1
    H3 -- "E(3)∩E(2) et E(3)∩E(4)" --> V2
    H6 -- "E(6)∩E(1) et E(6)∩E(4)" --> V4
    H7 -- "E(7)∩E(4) et E(7)∩E(5)" --> V5
import oscar.cp._
import oscar.cp.core.CPPropagStrength

object CrossWord extends CPModel with App {

  val alpha = Array(" ", "a", "b", "c", "d", "e", "f",
    "g", "h", "i", "j", "k", "l", "m",
    "n", "o", "p", "q", "r", "s", "t",
    "u", "v", "w", "x", "y", "z")

  val a = 1; val b = 2; val c = 3; val d = 4; val e = 5
  val f = 6; val g = 7; val h = 8; val i = 9; val j = 10
  val k = 11; val l = 12; val m = 13; val n = 14; val o = 15
  val p = 16; val q = 17; val r = 18; val s = 19; val t = 20
  val u = 21; val v = 22; val w = 23; val x = 24; val y = 25; val z = 26

  val AA = Array(
    Array(h, o, s, e, s),  // HOSES
    Array(l, a, s, e, r),  // LASER
    Array(s, a, i, l, s),  // SAILS
    Array(s, h, e, e, t),  // SHEET
    Array(s, t, e, e, r),  // STEER
    Array(h, e, e, l, 0),  // HEEL
    Array(h, i, k, e, 0),  // HIKE
    Array(k, e, e, l, 0),  // KEEL
    Array(k, n, o, t, 0),  // KNOT
    Array(l, i, n, e, 0),  // LINE
    Array(a, f, t, 0, 0),  // AFT
    Array(a, l, e, 0, 0),  // ALE
    Array(e, e, l, 0, 0),  // EEL
    Array(l, e, e, 0, 0),  // LEE
    Array(t, i, e, 0, 0))  // TIE

  val num_overlapping = 12
  val overlapping = Array(
    Array(0, 2, 1, 0), Array(0, 4, 2, 0),
    Array(3, 1, 1, 2), Array(3, 2, 4, 0), Array(3, 3, 2, 2),
    Array(6, 0, 1, 3), Array(6, 1, 4, 1), Array(6, 2, 2, 3),
    Array(7, 0, 5, 1), Array(7, 2, 1, 4), Array(7, 3, 4, 2), Array(7, 4, 2, 4))

  val N = 8
  val A = Array.fill(15, 5)(CPIntVar(0 to 26))
  val A_flatten = A.flatten
  val E = Array.fill(N)(CPIntVar(0 to 15))

  add(allDifferent(E))

  for (i <- 0 until 15; j <- 0 until 5)
    add(A(i)(j) == AA(i)(j))

  for (i <- 0 until num_overlapping) {
    add(
      A_flatten(E(overlapping(i)(0)) * 5 + overlapping(i)(1))
        ==
        A_flatten(E(overlapping(i)(2)) * 5 + overlapping(i)(3)),
      CPPropagStrength.Strong
    )
  }

  search(binaryStatic(E))

  onSolution {
    println("E: " + E.mkString(" "))
    for (ee <- 0 until N) {
      print(ee + ": (" + "%2d".format(E(ee).value) + ") ")
      for (ii <- 0 until 5)
        print(alpha(A(ee)(ii).value))
      println()
    }
  }

  val stats = start()
  println(stats)
}

🌘 Références

Abonne-toi au fil RSS pour ne rien manquer.

Étiquettes