From 311c21783de40b180542efdfa45ef9be023773f3 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 27 Apr 2023 21:27:34 +0200 Subject: [PATCH] tournament/swiss conditions refactoring WIP --- app/views/swiss/form.scala | 11 +- app/views/swiss/show.scala | 5 +- app/views/swiss/side.scala | 15 +- app/views/tournament/crud.scala | 5 +- app/views/tournament/form.scala | 6 +- modules/db/src/main/Handlers.scala | 2 + modules/gathering/src/main/Condition.scala | 10 +- .../src/main/ConditionHandlers.scala | 17 +- .../gathering/src/main/ConditionList.scala | 11 + .../gathering/src/main/GatheringClock.scala | 23 ++ modules/swiss/src/main/BsonHandlers.scala | 6 +- modules/swiss/src/main/SwissApi.scala | 12 +- modules/swiss/src/main/SwissCondition.scala | 254 ++---------------- modules/swiss/src/main/SwissForm.scala | 8 +- modules/swiss/src/main/SwissJson.scala | 3 +- .../src/main/SwissOfficialSchedule.scala | 4 +- .../tournament/src/main/BSONHandlers.scala | 2 +- .../src/main/TournamentCondition.scala | 10 +- .../tournament/src/main/TournamentForm.scala | 27 +- .../tournament/src/main/crud/CrudForm.scala | 11 +- 20 files changed, 134 insertions(+), 308 deletions(-) create mode 100644 modules/gathering/src/main/GatheringClock.scala diff --git a/app/views/swiss/form.scala b/app/views/swiss/form.scala index a86da2cef900d..c2316715ea9a9 100644 --- a/app/views/swiss/form.scala +++ b/app/views/swiss/form.scala @@ -6,8 +6,9 @@ import play.api.data.Form import lila.api.Context import lila.app.templating.Environment.{ given, * } import lila.app.ui.ScalatagsTemplate.{ *, given } -import lila.swiss.{ Swiss, SwissCondition, SwissForm } +import lila.swiss.{ Swiss, SwissForm } import lila.tournament.TournamentForm +import lila.gathering.{ ConditionForm, GatheringClock } object form: @@ -92,7 +93,7 @@ object form: frag( form3.split( form3.group(form("conditions.nbRatedGame.nb"), trans.minimumRatedGames(), half = true)( - form3.select(_, SwissCondition.DataForm.nbRatedGameChoices) + form3.select(_, ConditionForm.nbRatedGameChoices) ), (ctx.me.exists(_.hasTitle) || isGranted(_.ManageTournament)) ?? { form3.checkbox( @@ -105,10 +106,10 @@ object form: ), form3.split( form3.group(form("conditions.minRating.rating"), trans.minimumRating(), half = true)( - form3.select(_, SwissCondition.DataForm.minRatingChoices) + form3.select(_, ConditionForm.minRatingChoices) ), form3.group(form("conditions.maxRating.rating"), trans.maximumWeeklyRating(), half = true)( - form3.select(_, SwissCondition.DataForm.maxRatingChoices) + form3.select(_, ConditionForm.maxRatingChoices) ) ) ) @@ -164,7 +165,7 @@ final private class SwissFields(form: Form[SwissForm.SwissData], swiss: Option[S form3.select(_, SwissForm.clockLimitChoices, disabled = disabledAfterStart) ), form3.group(form("clock.increment"), trans.clockIncrement(), half = true)( - form3.select(_, TournamentForm.clockIncrementChoices, disabled = disabledAfterStart) + form3.select(_, GatheringClock.incrementChoices, disabled = disabledAfterStart) ) ) def roundInterval = diff --git a/app/views/swiss/show.scala b/app/views/swiss/show.scala index 1996065ac67d6..95b0df24fc8a5 100644 --- a/app/views/swiss/show.scala +++ b/app/views/swiss/show.scala @@ -8,10 +8,11 @@ import lila.api.Context import lila.app.templating.Environment.{ given, * } import lila.app.ui.ScalatagsTemplate.{ *, given } import lila.common.String.html.safeJsonValue -import lila.swiss.{ Swiss, SwissCondition } +import lila.swiss.Swiss import lila.swiss.SwissRoundNumber import lila.common.paginator.Paginator import lila.swiss.SwissPairing +import lila.gathering.Condition.WithVerdicts object show: @@ -19,7 +20,7 @@ object show: def apply( s: Swiss, - verdicts: SwissCondition.All.WithVerdicts, + verdicts: WithVerdicts, data: play.api.libs.json.JsObject, chatOption: Option[lila.chat.UserChat.Mine], streamers: List[UserId], diff --git a/app/views/swiss/side.scala b/app/views/swiss/side.scala index 81f9ae55b69f6..5fe8d63761950 100644 --- a/app/views/swiss/side.scala +++ b/app/views/swiss/side.scala @@ -7,7 +7,9 @@ import lila.api.Context import lila.app.templating.Environment.{ given, * } import lila.app.ui.ScalatagsTemplate.{ *, given } import lila.common.String.html.markdownLinksOrRichText -import lila.swiss.{ Swiss, SwissCondition } +import lila.swiss.Swiss +import lila.gathering.Condition +import lila.gathering.Condition.WithVerdicts object side: @@ -15,12 +17,10 @@ object side: def apply( s: Swiss, - verdicts: SwissCondition.All.WithVerdicts, + verdicts: WithVerdicts, streamers: List[UserId], chat: Boolean - )(using - ctx: Context - ) = + )(using ctx: Context) = frag( div(cls := "swiss__meta")( st.section(dataIcon := s.perfType.iconChar.toString)( @@ -61,8 +61,7 @@ object side: teamLink(s.teamId), if (verdicts.relevant) st.section( - dataIcon := (if (ctx.isAuth && verdicts.accepted) "" - else ""), + dataIcon := (if (ctx.isAuth && verdicts.accepted) "" else ""), cls := List( "conditions" -> true, "accepted" -> (ctx.isAuth && verdicts.accepted), @@ -80,7 +79,7 @@ object side: ), title := v.verdict.reason.map(_(ctx.lang)) )(v.verdict match { - case SwissCondition.RefusedUntil(until) => + case Condition.RefusedUntil(until) => frag( "Because you missed your last swiss game, you cannot enter a new swiss tournament until ", absClientInstant(until), diff --git a/app/views/tournament/crud.scala b/app/views/tournament/crud.scala index d7469b541c2bf..1b02fedc14d4e 100644 --- a/app/views/tournament/crud.scala +++ b/app/views/tournament/crud.scala @@ -10,6 +10,7 @@ import lila.app.ui.ScalatagsTemplate.{ *, given } import lila.common.paginator.Paginator import lila.tournament.crud.CrudForm import lila.tournament.{ Tournament, TournamentForm } +import lila.gathering.GatheringClock object crud: @@ -117,10 +118,10 @@ object crud: ), form3.split( form3.group(form("clockTime"), raw("Clock time"), half = true)( - form3.select(_, TournamentForm.clockTimeChoices) + form3.select(_, GatheringClock.timeChoices) ), form3.group(form("clockIncrement"), raw("Clock increment"), half = true)( - form3.select(_, TournamentForm.clockIncrementChoices) + form3.select(_, GatheringClock.incrementChoices) ) ), form3.split( diff --git a/app/views/tournament/form.scala b/app/views/tournament/form.scala index 1bb5701840d10..5512a11ea8107 100644 --- a/app/views/tournament/form.scala +++ b/app/views/tournament/form.scala @@ -9,7 +9,7 @@ import lila.app.templating.Environment.{ given, * } import lila.app.ui.ScalatagsTemplate.{ *, given } import lila.hub.LeaderTeam import lila.tournament.{ Tournament, TournamentForm } -import lila.gathering.{ Condition, ConditionForm } +import lila.gathering.{ Condition, ConditionForm, GatheringClock } object form: @@ -238,10 +238,10 @@ final private class TourFields(form: Form[?], tour: Option[Tournament])(using Co def clock = form3.split( form3.group(form("clockTime"), trans.clockInitialTime(), half = true)( - form3.select(_, TournamentForm.clockTimeChoices, disabled = disabledAfterStart) + form3.select(_, GatheringClock.timeChoices, disabled = disabledAfterStart) ), form3.group(form("clockIncrement"), trans.clockIncrement(), half = true)( - form3.select(_, TournamentForm.clockIncrementChoices, disabled = disabledAfterStart) + form3.select(_, GatheringClock.incrementChoices, disabled = disabledAfterStart) ) ) def minutes = diff --git a/modules/db/src/main/Handlers.scala b/modules/db/src/main/Handlers.scala index c4de27e0e7ee2..1c55d0ebc4cd7 100644 --- a/modules/db/src/main/Handlers.scala +++ b/modules/db/src/main/Handlers.scala @@ -121,6 +121,8 @@ trait Handlers: def typedMapHandlerIso[K, V: BSONHandler](using keyIso: StringIso[K]) = stringMapHandler[V].as[Map[K, V]](_.mapKeys(keyIso.from), _.mapKeys(keyIso.to)) + def ifPresentHandler[A](a: A) = quickHandler({ case _: BSONValue => a }, _ => BSONBoolean(true)) + given [T: BSONHandler]: BSONHandler[NonEmptyList[T]] = def listWriter = BSONWriter.collectionWriter[T, List[T]] def listReader = collectionReader[List, T] diff --git a/modules/gathering/src/main/Condition.scala b/modules/gathering/src/main/Condition.scala index 4a94b4c4f255c..3458b1cde273a 100644 --- a/modules/gathering/src/main/Condition.scala +++ b/modules/gathering/src/main/Condition.scala @@ -7,7 +7,7 @@ import lila.rating.PerfType import lila.user.{ Title, User } import lila.hub.LightTeam.TeamName -sealed trait Condition: +trait Condition: def name(perf: PerfType)(using Lang): String @@ -22,9 +22,11 @@ object Condition: type GetMaxRating = PerfType => Fu[IntRating] type GetUserTeamIds = User => Fu[List[TeamId]] - sealed abstract class Verdict(val accepted: Boolean, val reason: Option[Lang => String]) - case object Accepted extends Verdict(true, none) - case class Refused(because: Lang => String) extends Verdict(false, because.some) + enum Verdict(val accepted: Boolean, val reason: Option[Lang => String]): + case Accepted extends Verdict(true, none) + case Refused(because: Lang => String) extends Verdict(false, because.some) + case RefusedUntil(until: Instant) extends Verdict(false, none) + export Verdict.* case class WithVerdict(condition: Condition, verdict: Verdict) diff --git a/modules/gathering/src/main/ConditionHandlers.scala b/modules/gathering/src/main/ConditionHandlers.scala index 5ec88fa72bbd6..64a3ebc3820d4 100644 --- a/modules/gathering/src/main/ConditionHandlers.scala +++ b/modules/gathering/src/main/ConditionHandlers.scala @@ -3,6 +3,7 @@ package lila.gathering import lila.gathering.Condition.* import play.api.i18n.Lang import lila.rating.PerfType +import java.time.format.{ DateTimeFormatter, FormatStyle } object ConditionHandlers: @@ -14,12 +15,9 @@ object ConditionHandlers: given BSONDocumentHandler[NbRatedGame] = Macros.handler given BSONDocumentHandler[MaxRating] = Macros.handler given BSONDocumentHandler[MinRating] = Macros.handler - given BSONHandler[Titled.type] = quickHandler( - { case _: BSONValue => Titled }, - _ => BSONBoolean(true) - ) - given BSONDocumentHandler[TeamMember] = Macros.handler - given BSONDocumentHandler[AllowList] = Macros.handler + given BSONHandler[Titled.type] = ifPresentHandler(Titled) + given BSONDocumentHandler[TeamMember] = Macros.handler + given BSONDocumentHandler[AllowList] = Macros.handler object JSONHandlers: import lila.common.Json.given @@ -31,8 +29,13 @@ object ConditionHandlers: Json.obj( "condition" -> cond.name(pt), "verdict" -> verd.match - case Refused(reason) => reason(lang) case Accepted => JsString("ok") + case Refused(reason) => reason(lang) + case RefusedUntil(until) => + val date = DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) + .withLocale(lang.toLocale) + s"Because you missed your last swiss game, you cannot enter a new swiss tournament until $date" ) }, "accepted" -> verdicts.accepted diff --git a/modules/gathering/src/main/ConditionList.scala b/modules/gathering/src/main/ConditionList.scala index 3114238d65577..6f410f6634c48 100644 --- a/modules/gathering/src/main/ConditionList.scala +++ b/modules/gathering/src/main/ConditionList.scala @@ -4,6 +4,13 @@ import lila.gathering.Condition.* abstract class ConditionList(options: List[Option[Condition]]): + def maxRating: Option[MaxRating] + def minRating: Option[MinRating] + + def sameMaxRating(other: ConditionList) = maxRating.map(_.rating) == other.maxRating.map(_.rating) + def sameMinRating(other: ConditionList) = minRating.map(_.rating) == other.minRating.map(_.rating) + def sameRatings(other: ConditionList) = sameMaxRating(other) && sameMinRating(other) + lazy val list: List[Condition] = options.flatten def relevant = list.nonEmpty @@ -16,3 +23,7 @@ abstract class ConditionList(options: List[Option[Condition]]): case _: MaxRating => true case _: MinRating => true case _ => false + + def validRatings = (minRating, maxRating) match + case (Some(min), Some(max)) => min.rating < max.rating + case _ => true diff --git a/modules/gathering/src/main/GatheringClock.scala b/modules/gathering/src/main/GatheringClock.scala new file mode 100644 index 0000000000000..6451889657aee --- /dev/null +++ b/modules/gathering/src/main/GatheringClock.scala @@ -0,0 +1,23 @@ +package lila.gathering + +import chess.Clock +import chess.Clock.{ LimitSeconds, IncrementSeconds } +import lila.common.Form.* + +object GatheringClock: + + val times: Seq[Double] = Seq(0d, 1 / 4d, 1 / 2d, 3 / 4d, 1d, 3 / 2d) ++ { + (2 to 8 by 1) ++ (10 to 30 by 5) ++ (40 to 60 by 10) + }.map(_.toDouble) + val timeDefault = 2d + private def formatLimit(l: Double) = + Clock.Config(LimitSeconds((l * 60).toInt), IncrementSeconds(0)).limitString + { + if (l <= 1) " minute" else " minutes" + } + val timeChoices = optionsDouble(times, formatLimit) + + val increments = IncrementSeconds from { + (0 to 2 by 1) ++ (3 to 7) ++ (10 to 30 by 5) ++ (40 to 60 by 10) + } + val incrementDefault = IncrementSeconds(0) + val incrementChoices = options(IncrementSeconds raw increments, "%d second{s}") diff --git a/modules/swiss/src/main/BsonHandlers.scala b/modules/swiss/src/main/BsonHandlers.scala index c5cda2984b06e..045ceef818765 100644 --- a/modules/swiss/src/main/BsonHandlers.scala +++ b/modules/swiss/src/main/BsonHandlers.scala @@ -81,7 +81,7 @@ object BsonHandlers: isForfeit -> w.boolO(o.isForfeit) ) - import SwissCondition.BSONHandlers.given + import SwissCondition.bsonHandler given BSON[Swiss.Settings] with def reads(r: BSON.Reader) = @@ -93,7 +93,7 @@ object BsonHandlers: chatFor = r.intO("c") | Swiss.ChatFor.default, roundInterval = (r.intO("i") | 60).seconds, password = r.strO("p"), - conditions = r.getO[SwissCondition.All]("o") getOrElse SwissCondition.All.empty, + conditions = r.getD[SwissCondition.All]("o"), forbiddenPairings = r.getD[String]("fp"), manualPairings = r.getD[String]("mp") ) @@ -106,7 +106,7 @@ object BsonHandlers: "c" -> (s.chatFor != Swiss.ChatFor.default).option(s.chatFor), "i" -> s.roundInterval.toSeconds.toInt, "p" -> s.password, - "o" -> s.conditions.ifNonEmpty, + "o" -> s.conditions.relevant.option(s.conditions), "fp" -> s.forbiddenPairings.some.filter(_.nonEmpty), "mp" -> s.manualPairings.some.filter(_.nonEmpty) ) diff --git a/modules/swiss/src/main/SwissApi.scala b/modules/swiss/src/main/SwissApi.scala index a64ed7dc1e56e..4456f6ddafaa3 100644 --- a/modules/swiss/src/main/SwissApi.scala +++ b/modules/swiss/src/main/SwissApi.scala @@ -17,6 +17,7 @@ import lila.game.{ Game, Pov } import lila.round.actorApi.round.QuietFlag import lila.user.{ User, UserRepo } import lila.common.config.Max +import lila.gathering.Condition.WithVerdicts final class SwissApi( mongo: SwissMongo, @@ -74,7 +75,7 @@ final class SwissApi( chatFor = data.realChatFor, roundInterval = data.realRoundInterval, password = data.password, - conditions = data.conditions.all, + conditions = data.conditions, forbiddenPairings = ~data.forbiddenPairings, manualPairings = ~data.manualPairings ) @@ -106,7 +107,7 @@ final class SwissApi( if (data.roundInterval.isDefined) data.realRoundInterval else old.settings.roundInterval, password = data.password, - conditions = data.conditions.all, + conditions = data.conditions, forbiddenPairings = ~data.forbiddenPairings, manualPairings = ~data.manualPairings ) @@ -162,10 +163,9 @@ final class SwissApi( socket.reload(swiss.id) } - def verdicts(swiss: Swiss, me: Option[User]): Fu[SwissCondition.All.WithVerdicts] = - me match - case None => fuccess(swiss.settings.conditions.accepted) - case Some(user) => verify(swiss, user) + def verdicts(swiss: Swiss, me: Option[User]): Fu[WithVerdicts] = me match + case None => fuccess(swiss.settings.conditions.accepted) + case Some(user) => verify(swiss, user) def join(id: SwissId, me: User, isInTeam: TeamId => Boolean, password: Option[String]): Fu[Boolean] = Sequencing(id)(cache.swissCache.notFinishedById) { swiss => diff --git a/modules/swiss/src/main/SwissCondition.scala b/modules/swiss/src/main/SwissCondition.scala index 9ec2452746b03..ed98f4b8a7447 100644 --- a/modules/swiss/src/main/SwissCondition.scala +++ b/modules/swiss/src/main/SwissCondition.scala @@ -5,270 +5,74 @@ import play.api.i18n.Lang import lila.i18n.I18nKeys as trans import lila.rating.PerfType import lila.user.{ Title, User } - -sealed trait SwissCondition: - - def name(perf: PerfType)(using lang: Lang): String - - def withVerdict(verdict: SwissCondition.Verdict) = SwissCondition.WithVerdict(this, verdict) +import lila.gathering.{ Condition, ConditionList } +import lila.gathering.Condition.* +import alleycats.Zero object SwissCondition: - trait FlatCond: - - def apply(user: User, perf: PerfType): SwissCondition.Verdict - - type GetMaxRating = PerfType => Fu[IntRating] type GetBannedUntil = UserId => Fu[Option[Instant]] - enum Verdict(val accepted: Boolean, val reason: Option[Lang => String]): - case Accepted extends Verdict(true, none) - case Refused(because: Lang => String) extends Verdict(false, because.some) - case RefusedUntil(until: Instant) extends Verdict(false, none) - export Verdict.* - - case class WithVerdict(condition: SwissCondition, verdict: Verdict) - - case object PlayYourGames extends SwissCondition: + case object PlayYourGames extends Condition: def name(perf: PerfType)(using Lang) = "Play your games" def withBan(bannedUntil: Option[Instant]) = withVerdict { bannedUntil.fold[Verdict](Accepted)(RefusedUntil.apply) } - case object Titled extends SwissCondition with FlatCond: - def name(perf: PerfType)(using lang: Lang) = "Only titled players" - def apply(user: User, perf: PerfType) = - if (user.title.exists(_ != Title.LM)) Accepted - else Refused(lang => name(perf)(using lang)) - - case class NbRatedGame(nb: Int) extends SwissCondition with FlatCond: - - def apply(user: User, perf: PerfType) = - if (user.hasTitle) Accepted - else if (user.perfs(perf).nb >= nb) Accepted - else - Refused { lang => - given Lang = lang - val missing = nb - user.perfs(perf).nb - trans.needNbMorePerfGames.pluralTxt(missing, missing, perf.trans) - } - - def name(perf: PerfType)(using lang: Lang) = - trans.moreThanNbPerfRatedGames.pluralTxt(nb, nb, perf.trans) - - case class MaxRating(rating: IntRating) extends SwissCondition: - - def apply(perf: PerfType, getMaxRating: GetMaxRating)( - user: User - )(using Executor): Fu[Verdict] = - if (user.perfs(perf).provisional.yes) fuccess(Refused { lang => - given Lang = lang - trans.yourPerfRatingIsProvisional.txt(perf.trans) - }) - else if (user.perfs(perf).intRating > rating) fuccess(Refused { lang => - given Lang = lang - trans.yourPerfRatingIsTooHigh.txt(perf.trans, user.perfs(perf).intRating) - }) - else - getMaxRating(perf) map { - case r if r <= rating => Accepted - case r => - Refused { lang => - given Lang = lang - trans.yourTopWeeklyPerfRatingIsTooHigh.txt(perf.trans, r) - } - } - - def maybe(user: User, perf: PerfType): Boolean = - user.perfs(perf).provisional.no && user.perfs(perf).intRating <= rating - - def name(perf: PerfType)(using lang: Lang) = trans.ratedLessThanInPerf.txt(rating, perf.trans) - - case class MinRating(rating: IntRating) extends SwissCondition with FlatCond: - - def apply(user: User, perf: PerfType) = - if (user.perfs(perf).provisional.yes) Refused { lang => - given Lang = lang - trans.yourPerfRatingIsProvisional.txt(perf.trans) - } - else if (user.perfs(perf).intRating < rating) Refused { lang => - given Lang = lang - trans.yourPerfRatingIsTooLow.txt(perf.trans, user.perfs(perf).intRating) - } - else Accepted - - def name(perf: PerfType)(using lang: Lang) = trans.ratedMoreThanInPerf.txt(rating, perf.trans) - - case class AllowList(value: String) extends SwissCondition with FlatCond: - - private lazy val segments = value.linesIterator.map(_.trim.toLowerCase).toSet - - private def allowAnyTitledUser = segments contains "%titled" - - def apply(user: User, @annotation.nowarn perf: PerfType): SwissCondition.Verdict = - if (segments contains user.id.value) Accepted - else if (allowAnyTitledUser && user.hasTitle) Accepted - else Refused { _ => "Your name is not in the tournament line-up." } - - def name(perf: PerfType)(using lang: Lang) = "Fixed line-up" - case class All( nbRatedGame: Option[NbRatedGame], maxRating: Option[MaxRating], minRating: Option[MinRating], titled: Option[Titled.type], allowList: Option[AllowList], - playYourGames: Boolean = true - ): - - lazy val list: List[SwissCondition] = - List(nbRatedGame, maxRating, minRating, titled, allowList, playYourGames option PlayYourGames).flatten - - def relevant = list.nonEmpty - - def ifNonEmpty = list.nonEmpty option this + playYourGames: Option[PlayYourGames.type] = PlayYourGames.some + ) extends ConditionList(List(nbRatedGame, maxRating, minRating, titled, allowList, playYourGames)): def withVerdicts( perf: PerfType, getMaxRating: GetMaxRating, getBannedUntil: GetBannedUntil - )(user: User)(using Executor): Fu[All.WithVerdicts] = + )(user: User)(using Executor): Fu[WithVerdicts] = list.map { case PlayYourGames => getBannedUntil(user.id) map PlayYourGames.withBan - case c: MaxRating => c(perf, getMaxRating)(user) map c.withVerdict + case c: MaxRating => c(getMaxRating)(user, perf) map c.withVerdict case c: FlatCond => fuccess(c withVerdict c(user, perf)) - }.parallel dmap All.WithVerdicts.apply - - def accepted = All.WithVerdicts(list.map { WithVerdict(_, Accepted) }) - - def sameMaxRating(other: All) = maxRating.map(_.rating) == other.maxRating.map(_.rating) - def sameMinRating(other: All) = minRating.map(_.rating) == other.minRating.map(_.rating) - def sameRatings(other: All) = sameMaxRating(other) && sameMinRating(other) + }.parallel dmap WithVerdicts.apply def similar(other: All) = sameRatings(other) && titled == other.titled - def isRatingLimited = maxRating.isDefined || minRating.isDefined - object All: - val empty = All( - nbRatedGame = none, - maxRating = none, - minRating = none, - titled = none, - allowList = none, - playYourGames = false - ) - - case class WithVerdicts(list: List[WithVerdict]) extends AnyVal: - def relevant = list.nonEmpty - def accepted = list.forall(_.verdict.accepted) + val empty = All(none, none, none, none, none, none) + given zero: Zero[All] = Zero(empty) final class Verify(historyApi: lila.history.HistoryApi, banApi: SwissBanApi): - - def apply(swiss: Swiss, user: User)(using Executor): Fu[All.WithVerdicts] = + def apply(swiss: Swiss, user: User)(using Executor): Fu[WithVerdicts] = val getBan: GetBannedUntil = banApi.bannedUntil val getMaxRating: GetMaxRating = perf => historyApi.lastWeekTopRating(user, perf) swiss.settings.conditions.withVerdicts(swiss.perfType, getMaxRating, getBan)(user) - object BSONHandlers: - import reactivemongo.api.bson.* - import lila.db.dsl.{ *, given } - private given BSONDocumentHandler[NbRatedGame] = Macros.handler - private given BSONDocumentHandler[MaxRating] = Macros.handler - private given BSONDocumentHandler[MinRating] = Macros.handler - private given BSONHandler[Titled.type] = quickHandler[Titled.type]( - { case _: BSONValue => Titled }, - _ => BSONBoolean(true) - ) - private given BSONDocumentHandler[AllowList] = Macros.handler - given BSONDocumentHandler[All] = Macros.handler - - object DataForm: + object form: import play.api.data.Forms.* import lila.common.Form.* - val perfAuto = "auto" -> "Auto" - val perfKeys = "auto" :: PerfType.nonPuzzle.map(_.key) - def perfChoices(using lang: Lang) = - perfAuto :: PerfType.nonPuzzle.map { pt => - pt.key -> pt.trans - } - val nbRatedGames = Vector(0, 5, 10, 15, 20, 30, 40, 50, 75, 100, 150, 200) - val nbRatedGameChoices = options(nbRatedGames, "%d rated game{s}").map: - case (0, _) => (0, "No restriction") - case x => x - val nbRatedGame = mapping( - "nb" -> number(min = 0, max = ~nbRatedGames.lastOption) - )(NbRatedGame.apply)(_.nb.some) - case class RatingSetup(rating: Option[IntRating]): - def actualRating = rating.filter(r => r > 600 && r < 3000) - val maxRatings = - List(2200, 2100, 2000, 1900, 1800, 1700, 1600, 1500, 1400, 1300, 1200, 1100, 1000, 900, 800) - val maxRatingChoices = ("", "No restriction") :: - options(maxRatings, "Max rating of %d").toList.map { (k, v) => k.toString -> v } - val maxRating = mapping( - "rating" -> optional(numberIn(maxRatings).into[IntRating]) - )(RatingSetup.apply)(_.rating.some) - val minRatings = List(1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900, 2000, 2100, 2200, 2300, - 2400, 2500, 2600) - val minRatingChoices = ("", "No restriction") :: - options(minRatings, "Min rating of %d").toList.map { (k, v) => k.toString -> v } - val minRating = mapping( - "rating" -> optional(numberIn(minRatings).into[IntRating]) - )(RatingSetup.apply)(_.rating.some) + import lila.gathering.ConditionForm.* + + val playYourGames = + optional(boolean).transform(_.contains(true) option PlayYourGames, _.isDefined option true) + def all = mapping( "nbRatedGame" -> optional(nbRatedGame), - "maxRating" -> maxRating, - "minRating" -> minRating, - "titled" -> optional(boolean), + "maxRating" -> optional(maxRating), + "minRating" -> optional(minRating), + "titled" -> titled, "allowList" -> optional(allowList), - "playYourGames" -> optional(boolean) - )(AllSetup.apply)(unapply) + "playYourGames" -> playYourGames + )(All.apply)(unapply) .verifying("Invalid ratings", _.validRatings) - def allowList = - nonEmptyText(maxLength = 100_1000) - .transform[String](_.replace(',', '\n'), identity) - .transform[String](_.linesIterator.map(_.trim).filter(_.nonEmpty).distinct mkString "\n", identity) - .verifying("5000 usernames max", _.count('\n' == _) <= 5_000) - - case class AllSetup( - nbRatedGame: Option[NbRatedGame], - maxRating: RatingSetup, - minRating: RatingSetup, - titled: Option[Boolean], - allowList: Option[String], - playYourGames: Option[Boolean] - ): - - def validRatings = - (minRating.actualRating, maxRating.actualRating) match - case (Some(min), Some(max)) => min < max - case _ => true - - def all = All( - nbRatedGame.filter(_.nb > 0), - maxRating.actualRating map MaxRating.apply, - minRating.actualRating map MinRating.apply, - ~titled option Titled, - allowList = allowList map AllowList.apply, - playYourGames = ~playYourGames - ) - object AllSetup: - val default = AllSetup( - nbRatedGame = none, - maxRating = RatingSetup(none), - minRating = RatingSetup(none), - titled = none, - allowList = none, - playYourGames = true.some - ) - def apply(all: All): AllSetup = - AllSetup( - nbRatedGame = all.nbRatedGame, - maxRating = RatingSetup(all.maxRating.map(_.rating)), - minRating = RatingSetup(all.minRating.map(_.rating)), - titled = all.titled has Titled option true, - allowList = all.allowList.map(_.value), - playYourGames = all.playYourGames.some - ) + import reactivemongo.api.bson.* + given bsonHandler: BSONDocumentHandler[All] = + import lila.gathering.ConditionHandlers.BSONHandlers.given + import lila.db.dsl.ifPresentHandler + given BSONHandler[PlayYourGames.type] = ifPresentHandler(PlayYourGames) + Macros.handler diff --git a/modules/swiss/src/main/SwissForm.scala b/modules/swiss/src/main/SwissForm.scala index e833a92ec33ff..5b0e8d85ccc8f 100644 --- a/modules/swiss/src/main/SwissForm.scala +++ b/modules/swiss/src/main/SwissForm.scala @@ -33,7 +33,7 @@ final class SwissForm(using mode: Mode): "chatFor" -> optional(numberIn(chatForChoices.map(_._1))), "roundInterval" -> optional(numberIn(roundIntervals)), "password" -> optional(cleanNonEmptyText), - "conditions" -> SwissCondition.DataForm.all, + "conditions" -> SwissCondition.form.all, "forbiddenPairings" -> optional( cleanNonEmptyText.verifying( s"Maximum forbidden pairings: ${Swiss.maxForbiddenPairings}", @@ -70,7 +70,7 @@ final class SwissForm(using mode: Mode): chatFor = Swiss.ChatFor.default.some, roundInterval = Swiss.RoundInterval.auto.some, password = None, - conditions = SwissCondition.DataForm.AllSetup.default, + conditions = SwissCondition.All.empty, forbiddenPairings = none, manualPairings = none ) @@ -88,7 +88,7 @@ final class SwissForm(using mode: Mode): chatFor = s.settings.chatFor.some, roundInterval = s.settings.roundInterval.toSeconds.toInt.some, password = s.settings.password, - conditions = SwissCondition.DataForm.AllSetup(s.settings.conditions), + conditions = s.settings.conditions, forbiddenPairings = s.settings.forbiddenPairings.some.filter(_.nonEmpty), manualPairings = s.settings.manualPairings.some.filter(_.nonEmpty) ) @@ -168,7 +168,7 @@ object SwissForm: chatFor: Option[Int], roundInterval: Option[Int], password: Option[String], - conditions: SwissCondition.DataForm.AllSetup, + conditions: SwissCondition.All, forbiddenPairings: Option[String], manualPairings: Option[String] ): diff --git a/modules/swiss/src/main/SwissJson.scala b/modules/swiss/src/main/SwissJson.scala index 43d9b0b16122b..d0a28d208e66a 100644 --- a/modules/swiss/src/main/SwissJson.scala +++ b/modules/swiss/src/main/SwissJson.scala @@ -9,6 +9,7 @@ import lila.db.dsl.{ *, given } import lila.quote.Quote.given import lila.socket.{ SocketVersion, given } import lila.user.{ User, UserRepo } +import lila.gathering.Condition.WithVerdicts final class SwissJson( mongo: SwissMongo, @@ -35,7 +36,7 @@ final class SwissJson( swiss: Swiss, me: Option[User], isInTeam: Boolean, - verdicts: SwissCondition.All.WithVerdicts, + verdicts: WithVerdicts, reqPage: Option[Int] = None, // None = focus on me socketVersion: Option[SocketVersion] = None, playerInfo: Option[SwissPlayer.ViewExt] = None diff --git a/modules/swiss/src/main/SwissOfficialSchedule.scala b/modules/swiss/src/main/SwissOfficialSchedule.scala index 680b2a0a96de4..1a7503b9a8c93 100644 --- a/modules/swiss/src/main/SwissOfficialSchedule.scala +++ b/modules/swiss/src/main/SwissOfficialSchedule.scala @@ -3,6 +3,7 @@ package lila.swiss import chess.Clock.{ LimitSeconds, IncrementSeconds } import lila.db.dsl.{ *, given } +import lila.gathering.Condition.NbRatedGame final private class SwissOfficialSchedule(mongo: SwissMongo, cache: SwissCache)(using Executor @@ -81,8 +82,7 @@ final private class SwissOfficialSchedule(mongo: SwissMongo, cache: SwissCache)( position = none, roundInterval = SwissForm.autoInterval(config.clock), password = none, - conditions = SwissCondition - .All(nbRatedGame = SwissCondition.NbRatedGame(config.minGames).some, none, none, none, none), + conditions = SwissCondition.All.empty.copy(nbRatedGame = NbRatedGame(config.minGames).some), forbiddenPairings = "", manualPairings = "" ) diff --git a/modules/tournament/src/main/BSONHandlers.scala b/modules/tournament/src/main/BSONHandlers.scala index 620954704d5e7..da9409a00367d 100644 --- a/modules/tournament/src/main/BSONHandlers.scala +++ b/modules/tournament/src/main/BSONHandlers.scala @@ -48,7 +48,7 @@ object BSONHandlers: ) import lila.gathering.ConditionHandlers.BSONHandlers.given - import TournamentCondition.given + import TournamentCondition.bsonHandler given tourHandler: BSON[Tournament] with def reads(r: BSON.Reader) = diff --git a/modules/tournament/src/main/TournamentCondition.scala b/modules/tournament/src/main/TournamentCondition.scala index c2a8db5d98722..32bfba1017fed 100644 --- a/modules/tournament/src/main/TournamentCondition.scala +++ b/modules/tournament/src/main/TournamentCondition.scala @@ -38,16 +38,8 @@ object TournamentCondition: case c => fuccess(WithVerdict(c, Accepted)) }.parallel dmap WithVerdicts.apply - def sameMaxRating(other: All) = maxRating.map(_.rating) == other.maxRating.map(_.rating) - def sameMinRating(other: All) = minRating.map(_.rating) == other.minRating.map(_.rating) - def sameRatings(other: All) = sameMaxRating(other) && sameMinRating(other) - def similar(other: All) = sameRatings(other) && titled == other.titled && teamMember == other.teamMember - def validRatings = (minRating, maxRating) match - case (Some(min), Some(max)) => min.rating < max.rating - case _ => true - object All: val empty = All(none, none, none, none, none, none) given zero: Zero[All] = Zero(empty) @@ -79,6 +71,6 @@ object TournamentCondition: apply(conditions, user, perfType, getTeams).dmap(_.accepted) import reactivemongo.api.bson.* - given BSONDocumentHandler[All] = + given bsonHandler: BSONDocumentHandler[All] = import lila.gathering.ConditionHandlers.BSONHandlers.given Macros.handler diff --git a/modules/tournament/src/main/TournamentForm.scala b/modules/tournament/src/main/TournamentForm.scala index 241626c20eaba..a68ce0e0df5e6 100644 --- a/modules/tournament/src/main/TournamentForm.scala +++ b/modules/tournament/src/main/TournamentForm.scala @@ -14,17 +14,18 @@ import lila.common.Form.{ *, given } import lila.hub.LeaderTeam import lila.hub.LightTeam.* import lila.user.User -import lila.gathering.{ Condition, ConditionForm } +import lila.gathering.{ Condition, GatheringClock } final class TournamentForm: import TournamentForm.* + import GatheringClock.* def create(user: User, leaderTeams: List[LeaderTeam], teamBattleId: Option[TeamId] = None) = form(user, leaderTeams, none) fill TournamentSetup( name = teamBattleId.isEmpty option user.titleUsername, - clockTime = clockTimeDefault, - clockIncrement = clockIncrementDefault, + clockTime = timeDefault, + clockIncrement = incrementDefault, minutes = minuteDefault, waitMinutes = waitMinuteDefault.some, startDate = none, @@ -84,8 +85,8 @@ final class TournamentForm: private def makeMapping(user: User, leaderTeams: List[LeaderTeam]) = mapping( "name" -> optional(eventName(2, 30, user.isVerifiedOrAdmin)), - "clockTime" -> numberInDouble(clockTimeChoices), - "clockIncrement" -> numberIn(clockIncrementChoices).into[IncrementSeconds], + "clockTime" -> numberInDouble(timeChoices), + "clockIncrement" -> numberIn(incrementChoices).into[IncrementSeconds], "minutes" -> { if (lila.security.Granter(_.ManageTournament)(user)) number else numberIn(minuteChoices) @@ -113,22 +114,6 @@ object TournamentForm: import chess.variant.* - val clockTimes: Seq[Double] = Seq(0d, 1 / 4d, 1 / 2d, 3 / 4d, 1d, 3 / 2d) ++ { - (2 to 8 by 1) ++ (10 to 30 by 5) ++ (40 to 60 by 10) - }.map(_.toDouble) - val clockTimeDefault = 2d - private def formatLimit(l: Double) = - Clock.Config(LimitSeconds((l * 60).toInt), IncrementSeconds(0)).limitString + { - if (l <= 1) " minute" else " minutes" - } - val clockTimeChoices = optionsDouble(clockTimes, formatLimit) - - val clockIncrements = IncrementSeconds from { - (0 to 2 by 1) ++ (3 to 7) ++ (10 to 30 by 5) ++ (40 to 60 by 10) - } - val clockIncrementDefault = IncrementSeconds(0) - val clockIncrementChoices = options(IncrementSeconds raw clockIncrements, "%d second{s}") - val minutes = (20 to 60 by 5) ++ (70 to 120 by 10) ++ (150 to 360 by 30) ++ (420 to 600 by 60) :+ 720 val minuteDefault = 45 val minuteChoices = options(minutes, "%d minute{s}") diff --git a/modules/tournament/src/main/crud/CrudForm.scala b/modules/tournament/src/main/crud/CrudForm.scala index e5346247f6088..98f88e1d012e4 100644 --- a/modules/tournament/src/main/crud/CrudForm.scala +++ b/modules/tournament/src/main/crud/CrudForm.scala @@ -8,20 +8,21 @@ import chess.variant.Variant import chess.format.Fen import chess.Clock.{ LimitSeconds, IncrementSeconds } import lila.common.Form.{ given, * } -import lila.gathering.{ Condition, ConditionForm } +import lila.gathering.{ Condition, GatheringClock } final class CrudForm(repo: TournamentRepo): import CrudForm.* import TournamentForm.* + import GatheringClock.* def apply(tour: Option[Tournament]) = Form( mapping( "id" -> id[TourId](8, tour.map(_.id))(repo.exists), "name" -> text(minLength = 3, maxLength = 40), "homepageHours" -> number(min = 0, max = maxHomepageHours), - "clockTime" -> numberInDouble(clockTimeChoices), - "clockIncrement" -> numberIn(clockIncrementChoices).into[IncrementSeconds], + "clockTime" -> numberInDouble(timeChoices), + "clockIncrement" -> numberIn(incrementChoices).into[IncrementSeconds], "minutes" -> number(min = 20, max = 1440), "variant" -> typeIn(Variant.list.all.map(_.id).toSet), "position" -> optional(lila.common.Form.fen.playableStrict), @@ -42,8 +43,8 @@ final class CrudForm(repo: TournamentRepo): id = Tournament.makeId, name = "", homepageHours = 0, - clockTime = clockTimeDefault, - clockIncrement = clockIncrementDefault, + clockTime = timeDefault, + clockIncrement = incrementDefault, minutes = minuteDefault, variant = chess.variant.Standard.id, position = none,