diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala
index 4363f07c19ef3f7a50c2a72c76d7faf06d52a962..793605dbdd7022e4a87d746704bcb970a8b6f14d 100644
--- a/src/main/scala/Main.scala
+++ b/src/main/scala/Main.scala
@@ -1,20 +1,20 @@
 // Copyright (C) Maxime MORGE 2024
 import org.scata.core.AssignmentProblem
-import org.scata.algorithm.CBAA
+import org.scata.algorithm.CBBA
 import org.scata.patrol.Environment
 
 object Main {
   def main(args: Array[String]): Unit = {
 
     val nbRobots= 4
-    val nbTargets = 4
+    val nbTargets = 8
     val env = Environment.randomEnvironment(nbRobots, nbTargets)
     println(env)
-    val pb = env.toSAP()
+    val pb = env.toAP()
     println(pb)
-    val cbaa = new CBAA(pb)
+    val cbba = new CBBA(pb)
     println(pb)
-    val solution = cbaa.solve()
+    val solution = cbba.solve()
     println(solution)
   }
 }
\ No newline at end of file
diff --git a/src/main/scala/org/scata/algorithm/CBBA.scala b/src/main/scala/org/scata/algorithm/CBBA.scala
new file mode 100644
index 0000000000000000000000000000000000000000..cc888b457abe5a1096a0eb8822a12ef6e04e8dbd
--- /dev/null
+++ b/src/main/scala/org/scata/algorithm/CBBA.scala
@@ -0,0 +1,93 @@
+// Copyright (C) Maxime MORGE, 2024
+package org.scata.algorithm
+
+import org.scata.core._
+import scala.math.Ordering.Implicits.seqOrdering
+
+class CBBA(pb: AssignmentProblem) {
+  private val debug = true
+  private val maxTasksPerAgent = pb.tasks.size // You may define a limit Lt per agent
+
+  // Initialize structures
+  private var bundle: Map[Worker, List[Task]] = pb.workers.map(_ -> List.empty[Task]).toMap
+  private var path: Map[Worker, List[Task]] = pb.workers.map(_ -> List.empty[Task]).toMap
+  private var winningBid: Map[Task, (Worker, Int)] = pb.tasks.map(_ -> (NoWorker, Int.MaxValue)).toMap
+  private var winningAgent: Map[Task, Worker] = pb.tasks.map(_ -> NoWorker).toMap
+
+  private def marginalCost(worker: Worker, task: Task, currentPath: List[Task]): Int = {
+    // Here a simple cost (you can add a more complex scoring function)
+    pb.cost(worker, task)
+  }
+
+  /**
+   * Phase 1: Each agent builds a bundle of tasks greedily
+   */
+  private def buildBundle(worker: Worker): Unit = {
+    while (bundle(worker).size < maxTasksPerAgent) {
+      val candidateTasks = pb.tasks.diff(bundle(worker).toSet)
+      val costs = candidateTasks.map(task => task -> marginalCost(worker, task, path(worker))).toMap
+
+      val validTasks = costs.filter { case (task, cost) => cost < winningBid(task)._2 }
+
+      if (validTasks.nonEmpty) {
+        val bestTask = validTasks.minBy { case (task, gain) => (gain, task.name) }._1
+        bundle = bundle.updated(worker, bundle(worker) :+ bestTask)
+        path = path.updated(worker, path(worker) :+ bestTask)
+        winningBid = winningBid.updated(bestTask, (worker, costs(bestTask)))
+        winningAgent = winningAgent.updated(bestTask, worker)
+        if (debug) println(s"${worker.name} adds ${bestTask.name} to bundle with bid ${costs(bestTask)}")
+      } else return
+    }
+  }
+
+  /**
+   * Phase 2: Conflict resolution using consensus across all workers
+   */
+  private def resolveConflicts(): Boolean = {
+    var changed = false
+
+    for (task <- pb.tasks) {
+      val bids = pb.workers.map(worker =>
+        (worker, bundle(worker).indexOf(task)) match {
+          case (_, -1) => (worker, Int.MaxValue)
+          case (_, idx) => (worker, marginalCost(worker, task, path(worker)))
+        }
+      )
+      val (bestWorker, bestBid) = bids.minBy { case (w, b) => (b, w.name) }
+
+      if (winningAgent(task) != bestWorker) {
+        // Task is reallocated, remove from previous owner's bundle
+        val oldOwner = winningAgent(task)
+        bundle = bundle.updated(oldOwner, bundle(oldOwner).filterNot(_ == task))
+        path = path.updated(oldOwner, path(oldOwner).filterNot(_ == task))
+
+        bundle = bundle.updated(bestWorker, bundle(bestWorker) :+ task)
+        path = path.updated(bestWorker, path(bestWorker) :+ task)
+
+        winningAgent = winningAgent.updated(task, bestWorker)
+        winningBid = winningBid.updated(task, (bestWorker, bestBid))
+        changed = true
+        if (debug) println(s"Task ${task.name} reassigned to ${bestWorker.name} with bid $bestBid")
+      }
+    }
+
+    changed
+  }
+
+  /**
+   * Solve the assignment using CBBA
+   */
+  def solve(): MultipleAssignment = {
+    var converged = false
+    while (!converged) {
+      for (worker <- pb.workers) buildBundle(worker)
+      converged = !resolveConflicts()
+    }
+
+    val result = new MultipleAssignment(pb)
+    for ((worker, tasks) <- bundle; task <- tasks) {
+      result.bundle = result.bundle.updated(worker, result.bundle(worker) :+ task)
+    }
+    result
+  }
+}
\ No newline at end of file
diff --git a/src/main/scala/org/scata/patrol/Environment.scala b/src/main/scala/org/scata/patrol/Environment.scala
index 2d1fe6d285e0e92e67a237a321b1c5c7ebb66916..e1b0496a4f68804b0995c31a7f8c342af7e0e6b6 100644
--- a/src/main/scala/org/scata/patrol/Environment.scala
+++ b/src/main/scala/org/scata/patrol/Environment.scala
@@ -39,9 +39,7 @@ class Environment(val robots: SortedSet[Robot],
   /**
    * Generate a single assignment problem
    */
-  def toSAP(): AssignmentProblem = {
-    if (n > m)
-      throw new RuntimeException("Cannot generate a single assignment problem since there are more targets than robots")
+  def toAP()(): AssignmentProblem = {
 
     // Convert robots and targets into Workers and Tasks respectively
     val workers: SortedSet[Worker] = robots.map(r => new Worker(r.name))