tournament wip, add Pairing tests
This commit is contained in:
parent
b0076272b0
commit
f87619b465
|
@ -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))
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
52
app/tournament/Pairing.scala
Normal file
52
app/tournament/Pairing.scala
Normal 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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
23
app/views/tournament/show/started.scala.html
Normal file
23
app/views/tournament/show/started.scala.html
Normal 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
76
test/PairingTest.scala
Normal 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
|
||||
}
|
Loading…
Reference in a new issue