tournament wip, add Pairing tests

This commit is contained in:
Thibault Duplessis 2012-09-11 00:38:18 +02:00
parent b0076272b0
commit f87619b465
14 changed files with 334 additions and 102 deletions

View file

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

View file

@ -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)) {

View file

@ -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

View file

@ -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()
)
}

View file

@ -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])
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

@ -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") {
<h1 class="lichess_title">Tournaments</h1>
<div class="body">
list of tournaments
<table class="createds tournaments">
<thead>
<tr>
<th rowspan="4">Incoming tournaments</th>
</tr>
</thead>
<tbody>
@createds.map { tournament =>
<tr>
<td>
<a href="@routes.Tournament.show(tournament.id)">
@userIdToUsername(tournament.createdBy)'s tournament
</a>
</td>
<td>@tournament.showClock</td>
<td>@tournament.minutes minutes</td>
<td>@tournament.nbUsers / @tournament.minUsers players</td>
<td>
@if(ctx.me.fold(tournament.contains, false)) {
JOINED
}
</td>
</tr>
}
</tbody>
</table>
<table class="starteds tournaments">
<thead>
<tr>
<th rowspan="4">In progress</th>
</tr>
</thead>
<tbody>
@starteds.map { tournament =>
<tr>
<td>
<a href="@routes.Tournament.show(tournament.id)">
@userIdToUsername(tournament.createdBy)'s tournament
</a>
</td>
<td>@tournament.showClock</td>
<td>@tournament.minutes minutes</td>
<td>@tournament.nbUsers / @tournament.minUsers players</td>
<td>
@if(ctx.me.fold(tournament.contains, false)) {
JOINED
}
</td>
</tr>
}
</tbody>
</table>
</div>
<a href="@routes.Tournament.form()" class="action button">Create a new tournament</a>
<div>
<a href="@routes.Tournament.form()" class="action">Create a new tournament</a>
</div>
</div>
</div>
}

View file

@ -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) {
<h1>@title pending</h1>
<h1>@title</h1>
<div>
Waiting for @missingUsers players
Waiting for @tour.missingUsers players
</div>
<div class="user_list" data-href="@routes.Tournament.userList(id)">
<div class="user_list" data-href="@routes.Tournament.userList(tour.id)">
@tournament.userList(users)
</div>

View file

@ -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) {
<h1>@title</h1>
<div class="user_list" data-href="@routes.Tournament.userList(tour.id)">
@tournament.userList(users)
</div>
@ctx.me.filter(tour.contains).map { me =>
<div>
<form action="@routes.Tournament.withdraw(tour.id)" method="POST">
<input type="submit" class="submit button" value="Withdraw" />
</form>
</div>
}
}

76
test/PairingTest.scala Normal file
View file

@ -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
}