diff --git a/app/controllers/Tournament.scala b/app/controllers/Tournament.scala
index d5c8a14ff0..1310abee7c 100644
--- a/app/controllers/Tournament.scala
+++ b/app/controllers/Tournament.scala
@@ -2,7 +2,7 @@ package controllers
import lila._
import views._
-import tournament.{ Created }
+import tournament.{ Created, Started }
import http.Context
import scalaz.effects._
@@ -21,30 +21,49 @@ object Tournament extends LilaController {
private def userRepo = env.user.userRepo
val home = Open { implicit ctx ⇒
- IOk(repo.created map { tournaments ⇒
- html.tournament.home(tournaments)
- })
+ IOk(
+ for {
+ createds ← repo.created
+ starteds ← repo.started
+ } yield html.tournament.home(createds, starteds)
+ )
}
def show(id: String) = Open { implicit ctx ⇒
IOptionIOk(repo byId id) {
- case tour: Created ⇒ for {
- roomHtml ← messenger render tour
- users ← userRepo byIds tour.data.users
- } yield html.tournament.show.created(
- tour = tour,
- roomHtml = Html(roomHtml),
- version = version(tour.id),
- users = users
- )
- case _ ⇒ throw new Exception("oups")
+ case tour: Created ⇒ showCreated(tour)
+ case tour: Started ⇒ showStarted(tour)
+ case _ ⇒ throw new Exception("uuuh")
}
}
+ private def showCreated(tour: Created)(implicit ctx: Context) = for {
+ roomHtml ← messenger render tour
+ users ← userRepo byIds tour.data.users
+ } yield html.tournament.show.created(
+ tour = tour,
+ roomHtml = Html(roomHtml),
+ version = version(tour.id),
+ users = users)
+
+ private def showStarted(tour: Started)(implicit ctx: Context) = for {
+ roomHtml ← messenger render tour
+ users ← userRepo byIds tour.data.users
+ } yield html.tournament.show.started(
+ tour = tour,
+ roomHtml = Html(roomHtml),
+ version = version(tour.id),
+ users = users)
+
def join(id: String) = Auth { implicit ctx ⇒
implicit me ⇒
IOptionIORedirect(repo createdById id) { tour ⇒
- api.join(tour, me) map { _ ⇒ routes.Tournament.show(tour.id) }
+ api.join(tour, me) map { result ⇒
+ result.fold(
+ err ⇒ { println(err.shows); routes.Tournament.home() },
+ _ ⇒ routes.Tournament.show(tour.id)
+ )
+ }
}
}
@@ -75,7 +94,10 @@ object Tournament extends LilaController {
forms.create.bindFromRequest.fold(
err ⇒ io(BadRequest(html.message.form(err))),
setup ⇒ api.createTournament(setup, me).map(tournament ⇒
- Redirect(routes.Tournament.show(tournament.id))
+ tournament.fold(
+ err ⇒ { println(err.shows); Redirect(routes.Tournament.home()) },
+ tour ⇒ Redirect(routes.Tournament.show(tour.id))
+ )
))
}
}
diff --git a/app/core/Cron.scala b/app/core/Cron.scala
index 286f75a390..809937fc27 100644
--- a/app/core/Cron.scala
+++ b/app/core/Cron.scala
@@ -86,12 +86,12 @@ object Cron {
// also sent when the last player joins
message(8 seconds) {
- env.tournament.organizer -> tournament.StartTournament
+ env.tournament.organizer -> tournament.StartTournaments
}
// also sent when a player completes a game
message(5 seconds) {
- env.tournament.organizer -> tournament.StartPairing
+ env.tournament.organizer -> tournament.StartPairings
}
def message(freq: Duration)(to: (ActorRef, Any)) {
diff --git a/app/package.scala b/app/package.scala
index 0eb828770c..75d60f2799 100644
--- a/app/package.scala
+++ b/app/package.scala
@@ -34,7 +34,7 @@ package object lila
}
RegisterJodaTimeConversionHelpers()
- def !![A](msg: String) = msg.failNel[A]
+ def !![A](msg: String): Valid[A] = msg.failNel[A]
def nowMillis: Double = System.currentTimeMillis
def nowSeconds: Int = (nowMillis / 1000).toInt
diff --git a/app/tournament/Organizer.scala b/app/tournament/Organizer.scala
index ba970da7fc..0509db05b3 100644
--- a/app/tournament/Organizer.scala
+++ b/app/tournament/Organizer.scala
@@ -20,22 +20,24 @@ final class Organizer(
def receive = {
- case StartTournament ⇒ startTournament.unsafePerformIO
+ case StartTournaments ⇒ startTournaments.unsafePerformIO
+ case StartTournament(tour: Created) ⇒ api.start(tour).unsafePerformIO
- case StartPairing ⇒ startPairing.unsafePerformIO
+ case StartPairings ⇒ startPairings.unsafePerformIO
+ case StartPairing(tour: Started) ⇒ startPairing(tour).unsafePerformIO
}
- def startTournament = for {
- tours ← repo.created
- } yield (tours filter (_.readyToStart) map api.start).sequence
+ def startTournaments = repo.created flatMap { created =>
+ (created map api.start).sequence
+ }
- def startPairing = for {
- tours ← repo.started
- } yield (for {
- tour ← tours
- } yield Pairer(tour).toNel.fold(
- pairings ⇒ api.makePairings(tour, pairings),
- io()
- )).toList.sequence
+ def startPairings = repo.started flatMap { started =>
+ (started map startPairing).sequence
+ }
+ def startPairing(tour: Started) =
+ Pairing.createNewPairings(tour.users, tour.pairings).toNel.fold(
+ pairings ⇒ api.makePairings(tour, pairings),
+ io()
+ )
}
diff --git a/app/tournament/Pairer.scala b/app/tournament/Pairer.scala
deleted file mode 100644
index beb32b09ae..0000000000
--- a/app/tournament/Pairer.scala
+++ /dev/null
@@ -1,14 +0,0 @@
-package lila
-package tournament
-
-object Pairer {
-
- def apply(tour: Started): List[Pairing] =
- tour.pairings.fold(List[Pairing]()) {
- case (existing, pairings) if existing.status >= chess.Status.Mate =>
-
- case _ => pairings
- }
-
- private def idlePlayers(pairings: List[Pairings])
-}
diff --git a/app/tournament/Pairing.scala b/app/tournament/Pairing.scala
new file mode 100644
index 0000000000..7bba0e10f0
--- /dev/null
+++ b/app/tournament/Pairing.scala
@@ -0,0 +1,52 @@
+package lila
+package tournament
+
+import game.IdGenerator
+
+case class Pairing(
+ gameId: String,
+ status: chess.Status,
+ user1: String,
+ user2: String) {
+
+ def encode: RawPairing = RawPairing(gameId, status.id, users)
+
+ def users = List(user1, user2)
+ def usersPair = user1 -> user2
+ def contains(user: String) = user1 == user || user2 == user
+
+ def finished = status >= chess.Status.Mate
+ def playing = !finished
+
+ def withStatus(s: chess.Status) = copy(status = s)
+}
+
+case class RawPairing(g: String, s: Int, u: List[String]) {
+
+ def decode: Option[Pairing] = for {
+ status ← chess.Status(s)
+ user1 ← u.lift(0)
+ user2 ← u.lift(1)
+ } yield Pairing(g, status, user1, user2)
+}
+
+object Pairing {
+
+ def apply(user1: String, user2: String): Pairing = new Pairing(
+ gameId = IdGenerator.game,
+ status = chess.Status.Created,
+ user1 = user1,
+ user2 = user2)
+
+ def createNewPairings(users: List[String], pairings: Pairings): Pairings = {
+ val idleUsers: List[String] = pairings.filter(_.playing).foldLeft(users) {
+ (idles, pairing) ⇒ idles filterNot pairing.contains
+ }.toList
+ naivePairing(idleUsers).toList
+ }
+
+ private def naivePairing(users: List[String]) = users grouped 2 collect {
+ case List(u1, u2) ⇒ Pairing(u1, u2)
+ }
+
+}
diff --git a/app/tournament/Tournament.scala b/app/tournament/Tournament.scala
index ea4a2bc0ae..0c628dfcef 100644
--- a/app/tournament/Tournament.scala
+++ b/app/tournament/Tournament.scala
@@ -5,6 +5,7 @@ import org.joda.time.{ DateTime, Duration }
import org.scala_tools.time.Imports._
import com.novus.salat.annotations.Key
import ornicar.scalalib.OrnicarRandom
+import scalaz.NonEmptyList
import user.User
@@ -21,16 +22,18 @@ sealed trait Tournament {
val data: Data
def encode: RawTournament
- import data._
-
+ def minutes = data.minutes
lazy val duration = new Duration(minutes * 60 * 1000)
- def missingUsers = minUsers - users.size
-
+ def users = data.users
+ def nbUsers = users.size
+ def minUsers = data.minUsers
def contains(username: String): Boolean = users contains username
def contains(user: User): Boolean = contains(user.id)
+ def missingUsers = minUsers - users.size
def showClock = "2 + 0"
+ def createdBy = data.createdBy
}
case class Created(
@@ -67,32 +70,17 @@ case class Started(
startedAt: DateTime,
pairings: List[Pairing]) extends Tournament {
+ def addPairings(ps: NonEmptyList[Pairing]) =
+ copy(pairings = pairings ::: ps.list)
+
def encode = new RawTournament(
id = id,
- status = Status.Created.id,
+ status = Status.Started.id,
data = data,
startedAt = startedAt.some,
pairings = pairings map (_.encode))
}
-case class Pairing(
- gameId: String,
- status: chess.Status,
- users: List[String]) {
-
- def encode: RawPairing = RawPairing(gameId, status.id, users)
-}
-
-case class RawPairing(
- g: String,
- s: Int,
- u: List[String]) {
-
- def decode: Option[Pairing] = chess.Status(s) map { status ⇒
- Pairing(g, status, u)
- }
-}
-
case class RawTournament(
@Key("_id") id: String,
status: Int,
@@ -106,7 +94,7 @@ case class RawTournament(
def started: Option[Started] = for {
stAt ← startedAt
- if status == Status.Created.id
+ if status == Status.Started.id
} yield Started(
id = id,
data = data,
@@ -115,7 +103,11 @@ case class RawTournament(
def decodePairings = pairings map (_.decode) flatten
- def any: Option[Tournament] = created
+ def any: Option[Tournament] = Status(status) flatMap {
+ case Status.Created ⇒ created
+ case Status.Started ⇒ started
+ case _ ⇒ None
+ }
}
object Tournament {
@@ -141,5 +133,5 @@ object Tournament {
val minUsers = (2 to 4) ++ (5 to 30 by 5)
val minUserDefault = 10
- val minUserChoices = options(minUsers, "%d players{s}")
+ val minUserChoices = options(minUsers, "%d player{s}")
}
diff --git a/app/tournament/TournamentApi.scala b/app/tournament/TournamentApi.scala
index e574fa9a10..df8b7461a7 100644
--- a/app/tournament/TournamentApi.scala
+++ b/app/tournament/TournamentApi.scala
@@ -12,28 +12,45 @@ final class TournamentApi(
repo: TournamentRepo,
socket: Socket) {
- def makePairings(tour: Started, pairings: NonEmptyList[Pairing]): IO[Unit] = {
- io()
- }
+ def makePairings(tour: Started, pairings: NonEmptyList[Pairing]): IO[Unit] =
+ (tour addPairings pairings) |> { tour2 ⇒
+ for {
+ _ ← repo saveIO tour2
+ _ ← (pairings map { pairing ⇒
+ io() // create the game
+ }).sequence
+ } yield ()
+ }
- def createTournament(setup: TournamentSetup, me: User): IO[Created] =
- Tournament(
- createdBy = me.id,
- minutes = setup.minutes,
- minUsers = setup.minUsers
- ) |> { created ⇒
- repo saveIO created map (_ ⇒ created)
- }
+ def createTournament(setup: TournamentSetup, me: User): IO[Valid[Created]] = for {
+ hasTournament ← repo userHasRunningTournament me.id
+ tournament ← hasTournament.fold(
+ io(alreadyInATournament(me)),
+ Tournament(
+ createdBy = me.id,
+ minutes = setup.minutes,
+ minUsers = setup.minUsers
+ ) |> { created ⇒ repo saveIO created map (_ ⇒ created.success) }
+ )
+ } yield tournament
- def start(created: Created): IO[Unit] = repo saveIO created.start
+ def start(created: Created): IO[Unit] =
+ repo saveIO created.start doIf created.readyToStart
- def join(tour: Created, me: User): IO[Unit] = (tour join me).fold(
- err ⇒ putStrLn(err.shows),
- tour2 ⇒ for {
- _ ← repo saveIO tour2
- _ ← io(socket reloadUserList tour.id)
- } yield ()
- )
+ def join(tour: Created, me: User): IO[Valid[Unit]] = for {
+ hasTournament ← repo userHasRunningTournament me.id
+ tour2Valid = for {
+ tour2 ← tour join me
+ _ ← hasTournament.fold(alreadyInATournament(me), true.success)
+ } yield tour2
+ result ← tour2Valid.fold(
+ err ⇒ io(failure(err)),
+ tour2 ⇒ for {
+ _ ← repo saveIO tour2
+ _ ← io(socket reloadUserList tour.id)
+ } yield ().success
+ ): IO[Valid[Unit]]
+ } yield result
def withdraw(tour: Created, me: User): IO[Unit] = (tour withdraw me).fold(
err ⇒ putStrLn(err.shows),
@@ -42,4 +59,7 @@ final class TournamentApi(
_ ← io(socket reloadUserList tour.id)
} yield ()
)
+
+ private def alreadyInATournament[A](user: User) =
+ !![A]("%s already has a tournament in progress" format user.username)
}
diff --git a/app/tournament/TournamentRepo.scala b/app/tournament/TournamentRepo.scala
index 96ddee6f37..e38e6807d7 100644
--- a/app/tournament/TournamentRepo.scala
+++ b/app/tournament/TournamentRepo.scala
@@ -36,6 +36,12 @@ class TournamentRepo(collection: MongoCollection)
update(idSelector(tourId), $set("data.users" -> userIds.distinct))
}
+ def userHasRunningTournament(username: String): IO[Boolean] = io {
+ collection.findOne(
+ ("status" $ne Status.Finished.id) ++ ("data.users" -> username)
+ ).isDefined
+ }
+
def saveIO(tournament: Tournament): IO[Unit] = io {
save(tournament.encode)
}
diff --git a/app/tournament/actorApi.scala b/app/tournament/actorApi.scala
index 3be5bd7746..fdfc6a00f6 100644
--- a/app/tournament/actorApi.scala
+++ b/app/tournament/actorApi.scala
@@ -39,5 +39,7 @@ case object HubTimeout
case object GetNbHubs
// organizer
-case object StartTournament
-case object StartPairing
+case object StartTournaments
+case class StartTournament(tour: Created)
+case object StartPairings
+case class StartPairing(tour: Started)
diff --git a/app/views/tournament/home.scala.html b/app/views/tournament/home.scala.html
index 492c5fd6ab..eec53668e7 100644
--- a/app/views/tournament/home.scala.html
+++ b/app/views/tournament/home.scala.html
@@ -1,4 +1,4 @@
-@(tournaments: List[lila.tournament.Tournament])(implicit ctx: Context)
+@(createds: List[lila.tournament.Created], starteds: List[lila.tournament.Started])(implicit ctx: Context)
@tournament.layout(
title = "Tournaments") {
@@ -7,9 +7,62 @@ title = "Tournaments") {
Tournaments
- list of tournaments
+
+
+
+ Incoming tournaments |
+
+
+
+ @createds.map { tournament =>
+
+
+
+ @userIdToUsername(tournament.createdBy)'s tournament
+
+ |
+ @tournament.showClock |
+ @tournament.minutes minutes |
+ @tournament.nbUsers / @tournament.minUsers players |
+
+ @if(ctx.me.fold(tournament.contains, false)) {
+ JOINED
+ }
+ |
+
+ }
+
+
+
+
+
+ In progress |
+
+
+
+ @starteds.map { tournament =>
+
+
+
+ @userIdToUsername(tournament.createdBy)'s tournament
+
+ |
+ @tournament.showClock |
+ @tournament.minutes minutes |
+ @tournament.nbUsers / @tournament.minUsers players |
+
+ @if(ctx.me.fold(tournament.contains, false)) {
+ JOINED
+ }
+ |
+
+ }
+
+
- Create a new tournament
+
}
diff --git a/app/views/tournament/show/created.scala.html b/app/views/tournament/show/created.scala.html
index 8aefe21950..519a46699d 100644
--- a/app/views/tournament/show/created.scala.html
+++ b/app/views/tournament/show/created.scala.html
@@ -1,8 +1,6 @@
@(tour: lila.tournament.Created, roomHtml: Html, version: Int, users: List[User])(implicit ctx: Context)
-@import tour._
-
-@title = @{ tour.showClock + " tournament" }
+@title = @{ tour.showClock + " tournament pending" }
@tournament.show.layout(
tour = tour,
@@ -10,11 +8,11 @@ roomHtml = roomHtml,
version = version,
title = title) {
-@title pending
+@title
- Waiting for @missingUsers players
+ Waiting for @tour.missingUsers players
-
+
@tournament.userList(users)
diff --git a/app/views/tournament/show/started.scala.html b/app/views/tournament/show/started.scala.html
new file mode 100644
index 0000000000..265e2e4371
--- /dev/null
+++ b/app/views/tournament/show/started.scala.html
@@ -0,0 +1,23 @@
+@(tour: lila.tournament.Started, roomHtml: Html, version: Int, users: List[User])(implicit ctx: Context)
+
+@title = @{ tour.showClock + " tournament in progress" }
+
+@tournament.show.layout(
+tour = tour,
+roomHtml = roomHtml,
+version = version,
+title = title) {
+
+
@title
+
+ @tournament.userList(users)
+
+
+@ctx.me.filter(tour.contains).map { me =>
+
+
+
+}
+}
diff --git a/test/PairingTest.scala b/test/PairingTest.scala
new file mode 100644
index 0000000000..06e0ac9791
--- /dev/null
+++ b/test/PairingTest.scala
@@ -0,0 +1,76 @@
+package lila
+package tournament
+
+import tournament._
+
+class PairingTest extends LilaSpec {
+
+ import Pairing._
+
+ val u1 = "u1"
+ val u2 = "u2"
+ val u3 = "u3"
+ val u4 = "u4"
+ val u5 = "u5"
+
+ "Create new pairings" should {
+ "2 players" in {
+ val users = List(u1, u2)
+ "first pairing" in {
+ val pairings = List[Pairing]()
+ pairs(users, pairings) must_== List(u1 -> u2)
+ }
+ "finished pairing" in {
+ val pairings = List(mate(u1, u2))
+ pairs(users, pairings) must_== List(u1 -> u2)
+ }
+ "started pairing" in {
+ val pairings = List(started(u1, u2))
+ pairs(users, pairings) must_== Nil
+ }
+ "finished and started pairing" in {
+ val pairings = List(mate(u1, u2), started(u1, u2))
+ pairs(users, pairings) must_== Nil
+ }
+ }
+ }
+ "3 players" should {
+ val users = List(u1, u2, u3)
+ "first pairing" in {
+ val pairings = List[Pairing]()
+ pairs(users, pairings) must beOneOf(List(u1 -> u2), List(u1 -> u3), List(u2 -> u3))
+ }
+ "finished pairing" in {
+ val pairings = List(mate(u1, u2))
+ pairs(users, pairings) must_== List(u1 -> u3)
+ }
+ "started pairing" in {
+ val pairings = List(started(u1, u2))
+ pairs(users, pairings) must_== Nil
+ }
+ "finished and started pairing" in {
+ val pairings = List(mate(u1, u2), started(u1, u3))
+ pairs(users, pairings) must_== Nil
+ }
+ "many finished pairings" in {
+ "without ambiguity" in {
+ val pairings = List(mate(u1, u2), mate(u1, u3))
+ pairs(users, pairings) must_== List(u2 -> u3)
+ }
+ "favor longer idle" in {
+ val pairings = List(mate(u1, u2), mate(u1, u3), mate(u2, u3))
+ pairs(users, pairings) must_== List(u1 -> u2)
+ }
+ }
+ }
+
+ private def pairs(users: List[String], pairings: Pairings) =
+ createNewPairings(users, pairings) map (_.usersPair) map {
+ case (a, b) ⇒ (a < b).fold(a -> b, b -> a)
+ }
+
+ private def started(a: String, b: String) =
+ Pairing(a, b) withStatus chess.Status.Started
+ private def mate(a: String, b: String) =
+ Pairing(a, b) withStatus chess.Status.Mate
+}