Rewrite trophy code: don't hardcode kinds

instead of hardcoding trophy kinds in the code, all of them go in a database collection 'trophyKind'

to initialize, run the create-trophy-kinds.js script

no changes to the 'trophy' collection will be needed whatsoever
glicko-over-time
ProgramFOX 2019-06-29 11:19:12 -04:00 committed by Thibault Duplessis
parent 5f69d01369
commit 1a909ffacb
8 changed files with 267 additions and 264 deletions

View File

@ -9,7 +9,7 @@ import lila.forum.PostApi
import lila.game.Crosstable
import lila.relation.RelationApi
import lila.security.Granter
import lila.user.{ User, Trophy, Trophies, TrophyApi }
import lila.user.{ User, SimplifiedTrophy, Trophy, Trophies, TrophyApi }
case class UserInfo(
user: User,
@ -36,28 +36,7 @@ case class UserInfo(
def completionRatePercent = completionRate.map { cr => math.round(cr * 100) }
lazy val allTrophies = List(
Granter(_.PublicMod)(user) option Trophy(
_id = "",
user = user.id,
kind = Trophy.Kind.Moderator,
date = org.joda.time.DateTime.now
),
Granter(_.Developer)(user) option Trophy(
_id = "",
user = user.id,
kind = Trophy.Kind.Developer,
date = org.joda.time.DateTime.now
),
Granter(_.Verified)(user) option Trophy(
_id = "",
user = user.id,
kind = Trophy.Kind.Verified,
date = org.joda.time.DateTime.now
)
).flatten ::: trophies
def countTrophiesAndPerfCups = allTrophies.size + ranks.??(_.count(_._2 <= 100))
def countTrophiesAndPerfCups = trophies.size + ranks.??(_.count(_._2 <= 100))
}
object UserInfo {
@ -146,36 +125,42 @@ object UserInfo {
postApi.nbByUser(user.id) zip
studyRepo.countByOwner(user.id) zip
trophyApi.findByUser(user) zip
shieldApi.active(user) zip
revolutionApi.active(user) zip
fetchTeamIds(user.id) zip
fetchIsCoach(user) zip
fetchIsStreamer(user) zip
(user.count.rated >= 10).??(insightShare.grant(user, ctx.me)) zip
getPlayTime(user) zip
completionRate(user.id) flatMap {
case ranks ~ ratingChart ~ nbFollowers ~ nbBlockers ~ nbPosts ~ nbStudies ~ trophies ~ shields ~ revols ~ teamIds ~ isCoach ~ isStreamer ~ insightVisible ~ playTime ~ completionRate =>
(nbs.playing > 0) ?? isHostingSimul(user.id) map { hasSimul =>
new UserInfo(
user = user,
ranks = ranks,
nbs = nbs,
hasSimul = hasSimul,
ratingChart = ratingChart,
nbFollowers = nbFollowers,
nbBlockers = nbBlockers,
nbPosts = nbPosts,
nbStudies = nbStudies,
playTime = playTime,
trophies = trophies,
shields = shields,
revolutions = revols,
teamIds = teamIds,
isStreamer = isStreamer,
isCoach = isCoach,
insightVisible = insightVisible,
completionRate = completionRate
)
}
}
trophyApi.roleBasedTrophies(
user,
Granter(_.PublicMod)(user),
Granter(_.Developer)(user),
Granter(_.Verified)(user)
) zip
shieldApi.active(user) zip
revolutionApi.active(user) zip
fetchTeamIds(user.id) zip
fetchIsCoach(user) zip
fetchIsStreamer(user) zip
(user.count.rated >= 10).??(insightShare.grant(user, ctx.me)) zip
getPlayTime(user) zip
completionRate(user.id) flatMap {
case ranks ~ ratingChart ~ nbFollowers ~ nbBlockers ~ nbPosts ~ nbStudies ~ trophies ~ roleTrophies ~ shields ~ revols ~ teamIds ~ isCoach ~ isStreamer ~ insightVisible ~ playTime ~ completionRate =>
(nbs.playing > 0) ?? isHostingSimul(user.id) map { hasSimul =>
new UserInfo(
user = user,
ranks = ranks,
nbs = nbs,
hasSimul = hasSimul,
ratingChart = ratingChart,
nbFollowers = nbFollowers,
nbBlockers = nbBlockers,
nbPosts = nbPosts,
nbStudies = nbStudies,
playTime = playTime,
trophies = trophies ::: roleTrophies,
shields = shields,
revolutions = revols,
teamIds = teamIds,
isStreamer = isStreamer,
isCoach = isCoach,
insightVisible = insightVisible,
completionRate = completionRate
)
}
}
}

View File

@ -3,8 +3,7 @@ package views.html.user.show
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.user.Trophy
import Trophy.Kind
import lila.user.{ Trophy, TrophyKind }
import lila.user.User
import controllers.routes
@ -12,7 +11,7 @@ import controllers.routes
object otherTrophies {
def apply(u: User, info: lila.app.mashup.UserInfo)(implicit ctx: Context) = frag(
info.allTrophies.filter(_.kind.klass.has("fire-trophy")).some.filter(_.nonEmpty) map { trophies =>
info.trophies.filter(_.kind.klass.has("fire-trophy")).some.filter(_.nonEmpty) map { trophies =>
div(cls := "stacked")(
trophies.sorted.map { trophy =>
trophy.kind.icon.map { iconChar =>
@ -39,7 +38,7 @@ object otherTrophies {
href := routes.Tournament.show(revol.tourId)
)(revol.iconChar.toString)
},
info.allTrophies.find(_.kind == Kind.ZugMiracle).map { t =>
info.trophies.find(_.kind._id == TrophyKind.zugMiracle).map { t =>
frag(
styleTag("""
.trophy.zugMiracle {
@ -53,7 +52,7 @@ object otherTrophies {
height: 60px;
}
@keyframes psyche {
100% { filter: hue-rotate(360deg); }
100% { filter: hue-rotate(360deg); }
}
.trophy.zugMiracle:hover {
transform: translateY(-9px);
@ -64,15 +63,13 @@ object otherTrophies {
)
)
},
info.allTrophies.filter(t => t.kind == Kind.ZHWC17 || t.kind == Kind.ZHWC18).::: {
info.allTrophies.filter(t => t.kind == Kind.AtomicWC16 || t.kind == Kind.AtomicWC17 || t.kind == Kind.AtomicWC18)
}.map { t =>
info.trophies.filter(_.kind.withCustomImage).map { t =>
a(awardCls(t), href := t.kind.url, ariaTitle(t.kind.name),
style := "width: 65px; margin: 0 3px!important;")(
img(src := staticUrl(s"images/trophy/${t.kind.key}.png"), width := 65, height := 80)
img(src := staticUrl(s"images/trophy/${t.kind._id}.png"), width := 65, height := 80)
)
},
info.allTrophies.filter(_.kind.klass.has("icon3d")).sorted.map { trophy =>
info.trophies.filter(_.kind.klass.has("icon3d")).sorted.map { trophy =>
trophy.kind.icon.map { iconChar =>
a(
awardCls(trophy),
@ -98,5 +95,5 @@ object otherTrophies {
)("")
)
private def awardCls(t: Trophy) = cls := s"trophy award ${t.kind.key} ${~t.kind.klass}"
private def awardCls(t: Trophy) = cls := s"trophy award ${t.kind._id} ${~t.kind.klass}"
}

View File

@ -0,0 +1,128 @@
db.trophyKind.drop();
db.trophyKind.insert({
_id: "zugMiracle",
name: "Zug miracle",
url: "//lichess.org/faq#trophies",
order: NumberInt(1),
withCustomImage: false
});
db.trophyKind.insert({
_id: "wayOfBerserk",
name: "The way of Berserk",
icon: "`",
url: "//lichess.org/faq#trophies",
klass: "fire-trophy",
order: NumberInt(2),
withCustomImage: false
});
db.trophyKind.insert({
_id: "marathonWinner",
name: "Marathon Winner",
icon: "\\",
klass: "fire-trophy",
order: NumberInt(3),
withCustomImage: false
});
db.trophyKind.insert({
_id: "marathonTopTen",
name: "Marathon Top 10",
icon: "\\",
klass: "fire-trophy",
order: NumberInt(4),
withCustomImage: false
});
db.trophyKind.insert({
_id: "marathonTopFifty",
name: "Marathon Top 50",
icon: "\\",
klass: "fire-trophy",
order: NumberInt(5),
withCustomImage: false
});
db.trophyKind.insert({
_id: "marathonTopHundred",
name: "Marathon Top 100",
icon: "\\",
klass: "fire-trophy",
order: NumberInt(6),
withCustomImage: false
});
db.trophyKind.insert({
_id: "marathonSurvivor",
name: "Marathon #1 survivor",
icon: ",",
url: "//lichess.org/blog/VXF45yYAAPQgLH4d/chess-marathon-1",
klass: "fire-trophy",
order: NumberInt(7),
withCustomImage: false
});
db.trophyKind.insert({
_id: "bongcloudWarrior",
name: "Bongcloud Warrior",
icon: "~",
url: "//lichess.org/forum/lichess-feedback/bongcloud-trophy",
klass: "fire-trophy",
order: NumberInt(8),
withCustomImage: false
});
db.trophyKind.insert({
_id: "developer",
name: "Lichess developer",
icon: "\ue000",
url: "https://github.com/ornicar/lila/graphs/contributors",
klass: "icon3d",
order: NumberInt(100),
withCustomImage: false
});
db.trophyKind.insert({
_id: "moderator",
name: "Lichess moderator",
icon: "\ue002",
url: "//lichess.org/report",
klass: "icon3d",
order: NumberInt(101),
withCustomImage: false
});
db.trophyKind.insert({
_id: "verified",
name: "Verified account",
icon: "E",
klass: "icon3d",
order: NumberInt(102),
withCustomImage: false
});
db.trophyKind.insert({
_id: "zhwc17",
name: "Crazyhouse champion 2017",
url: "//lichess.org/blog/WMnMzSEAAMgA3oAW/crazyhouse-world-championship-the-candidates",
order: NumberInt(1),
withCustomImage: true
});
db.trophyKind.insert({
_id: "zhwc18",
name: "Crazyhouse champion 2018",
url: "//lichess.org/forum/team-crazyhouse-world-championship/opperwezen-the-2nd-cwc",
order: NumberInt(1),
withCustomImage: true
});
db.trophyKind.insert({
_id: "atomicwc16",
name: "Atomic World Champion 2016",
url: "//lichess.org/forum/team-atomic-wc/championship-final",
order: NumberInt(1),
withCustomImage: true
});
db.trophyKind.insert({
_id: "atomicwc17",
name: "Atomic World Champion 2017",
url: "//lichess.org/forum/team-atomic-wc/awc-2017-its-final-time",
order: NumberInt(1),
withCustomImage: true
});
db.trophyKind.insert({
_id: "atomicwc18",
name: "Atomic World Champion 2018",
url: "//lichess.org/forum/team-atomic-wc/announcement-awc-2018",
order: NumberInt(1),
withCustomImage: true
});

View File

@ -556,6 +556,7 @@ user {
user = user4
note = note
trophy = trophy
trophyKind = trophyKind
ranking = ranking
}
password.bpass {

View File

@ -176,17 +176,19 @@ final class TournamentApi(
else if (tour.isCreated) wipe(tour)
}
private def awardTrophies(tour: Tournament): Funit =
private def awardTrophies(tour: Tournament): Funit = {
import lila.user.TrophyKind._
tour.schedule.??(_.freq == Schedule.Freq.Marathon) ?? {
PlayerRepo.bestByTourWithRank(tour.id, 100).flatMap {
_.map {
case rp if rp.rank == 1 => trophyApi.award(rp.player.userId, _.MarathonWinner)
case rp if rp.rank <= 10 => trophyApi.award(rp.player.userId, _.MarathonTopTen)
case rp if rp.rank <= 50 => trophyApi.award(rp.player.userId, _.MarathonTopFifty)
case rp => trophyApi.award(rp.player.userId, _.MarathonTopHundred)
case rp if rp.rank == 1 => trophyApi.award(rp.player.userId, marathonWinner)
case rp if rp.rank <= 10 => trophyApi.award(rp.player.userId, marathonTopTen)
case rp if rp.rank <= 50 => trophyApi.award(rp.player.userId, marathonTopFifty)
case rp => trophyApi.award(rp.player.userId, marathonTopHundred)
}.sequenceFu.void
}
}
}
def verdicts(tour: Tournament, me: Option[User], getUserTeamIds: User => Fu[TeamIdList]): Fu[Condition.All.WithVerdicts] = me match {
case None => fuccess(tour.conditions.accepted)

View File

@ -23,6 +23,7 @@ final class Env(
val CollectionUser = config getString "collection.user"
val CollectionNote = config getString "collection.note"
val CollectionTrophy = config getString "collection.trophy"
val CollectionTrophyKind = config getString "collection.trophyKind"
val CollectionRanking = config getString "collection.ranking"
val PasswordBPassSecret = config getString "password.bpass.secret"
}
@ -41,7 +42,7 @@ final class Env(
lazy val noteApi = new NoteApi(db(CollectionNote), timeline, system.lilaBus)
lazy val trophyApi = new TrophyApi(db(CollectionTrophy))
lazy val trophyApi = new TrophyApi(db(CollectionTrophy), db(CollectionTrophyKind))
lazy val rankingApi = new RankingApi(db(CollectionRanking), mongoCache, asyncCache, lightUser)

View File

@ -2,10 +2,26 @@ package lila.user
import org.joda.time.DateTime
case class SimplifiedTrophy(
_id: String,
user: String,
kind: String,
date: DateTime
)
object SimplifiedTrophy {
def make(userId: String, kindKey: String): SimplifiedTrophy = SimplifiedTrophy(
_id = ornicar.scalalib.Random nextString 8,
user = userId,
kind = kindKey,
date = DateTime.now
)
}
case class Trophy(
_id: String, // random
user: String,
kind: Trophy.Kind,
kind: TrophyKind,
date: DateTime
) extends Ordered[Trophy] {
@ -16,179 +32,24 @@ case class Trophy(
else Integer.compare(kind.order, other.kind.order)
}
object Trophy {
case class TrophyKind(
_id: String,
name: String,
icon: Option[String],
url: Option[String],
klass: Option[String],
order: Int,
withCustomImage: Boolean
)
sealed abstract class Kind(
val key: String,
val name: String,
val icon: Option[String],
val url: Option[String],
val klass: Option[String],
val order: Int
)
object Kind {
object ZugMiracle extends Kind(
key = "zugMiracle",
name = "Zug miracle",
icon = none,
url = "//lichess.org/qa/259/how-do-you-get-a-zug-miracle-trophy".some,
klass = none,
order = 1
)
object WayOfBerserk extends Kind(
key = "wayOfBerserk",
name = "The way of Berserk",
icon = "`".some,
url = "//lichess.org/qa/340/way-of-berserk-trophy".some,
klass = "fire-trophy".some,
order = 2
)
object MarathonWinner extends Kind(
key = "marathonWinner",
name = "Marathon Winner",
icon = "\\".some,
url = none,
klass = "fire-trophy".some,
order = 3
)
object MarathonTopTen extends Kind(
key = "marathonTopTen",
name = "Marathon Top 10",
icon = "\\".some,
url = none,
klass = "fire-trophy".some,
order = 4
)
object MarathonTopFifty extends Kind(
key = "marathonTopFifty",
name = "Marathon Top 50",
icon = "\\".some,
url = none,
klass = "fire-trophy".some,
order = 5
)
object MarathonTopHundred extends Kind(
key = "marathonTopHundred",
name = "Marathon Top 100",
icon = "\\".some,
url = none,
klass = "fire-trophy".some,
order = 6
)
object MarathonSurvivor extends Kind(
key = "marathonSurvivor",
name = "Marathon #1 survivor",
icon = ",".some,
url = "//lichess.org/blog/VXF45yYAAPQgLH4d/chess-marathon-1".some,
klass = "fire-trophy".some,
order = 7
)
object BongcloudWarrior extends Kind(
key = "bongcloudWarrior",
name = "Bongcloud Warrior",
icon = "~".some,
url = "//lichess.org/forum/lichess-feedback/bongcloud-trophy".some,
klass = "fire-trophy".some,
order = 8
)
object Developer extends Kind(
key = "developer",
name = "Lichess developer",
icon = "".some,
url = "https://github.com/ornicar/lila/graphs/contributors".some,
klass = "icon3d".some,
order = 100
)
object Moderator extends Kind(
key = "moderator",
name = "Lichess moderator",
icon = "".some,
url = "//lichess.org/report".some,
"icon3d".some,
order = 101
)
object Verified extends Kind(
key = "verified",
name = "Verified account",
icon = "E".some,
url = none,
"icon3d".some,
order = 102
)
object ZHWC17 extends Kind(
key = "zhwc17",
name = "Crazyhouse champion 2017",
icon = none,
url = "//lichess.org/blog/WMnMzSEAAMgA3oAW/crazyhouse-world-championship-the-candidates".some,
klass = none,
order = 1
)
object ZHWC18 extends Kind(
key = "zhwc18",
name = "Crazyhouse champion 2018",
icon = none,
url = "//lichess.org/forum/team-crazyhouse-world-championship/opperwezen-the-2nd-cwc".some,
klass = none,
order = 1
)
object AtomicWC16 extends Kind(
key = "atomicwc16",
name = "Atomic World Champion 2016",
icon = none,
url = "//lichess.org/forum/team-atomic-wc/championship-final".some,
klass = none,
order = 1
)
object AtomicWC17 extends Kind(
key = "atomicwc17",
name = "Atomic World Champion 2017",
icon = none,
url = "//lichess.org/forum/team-atomic-wc/awc-2017-its-final-time".some,
klass = none,
order = 1
)
object AtomicWC18 extends Kind(
key = "atomicwc18",
name = "Atomic World Champion 2018",
icon = none,
url = "//lichess.org/forum/team-atomic-wc/announcement-awc-2018".some,
klass = none,
order = 1
)
val all = List(
Developer, Moderator, Verified,
MarathonTopHundred, MarathonTopTen, MarathonTopFifty, MarathonWinner,
ZugMiracle, ZHWC17, ZHWC18,
WayOfBerserk,
MarathonSurvivor,
BongcloudWarrior,
AtomicWC16, AtomicWC17, AtomicWC18
)
val byKey: Map[String, Kind] = all.map { k => k.key -> k }(scala.collection.breakOut)
}
def make(userId: String, kind: Trophy.Kind) = Trophy(
_id = ornicar.scalalib.Random nextString 8,
user = userId,
kind = kind,
date = DateTime.now
)
object TrophyKind {
val marathonWinner = "marathonWinner"
val marathonTopTen = "marathonTopTen"
val marathonTopFifty = "marathonTopFifty"
val marathonTopHundred = "marathonTopHundred"
val moderator = "moderator"
val developer = "developer"
val verified = "verified"
val zugMiracle = "zugMiracle"
}

View File

@ -4,23 +4,51 @@ import lila.db.dsl._
import lila.db.BSON.BSONJodaDateTimeHandler
import reactivemongo.bson._
final class TrophyApi(coll: Coll) {
private implicit val trophyKindBSONHandler = new BSONHandler[BSONString, Trophy.Kind] {
def read(bsonString: BSONString): Trophy.Kind =
Trophy.Kind.byKey get bsonString.value err s"No such trophy kind: ${bsonString.value}"
def write(x: Trophy.Kind) = BSONString(x.key)
}
private implicit val trophyBSONHandler = Macros.handler[Trophy]
def award(userId: String, kind: Trophy.Kind): Funit =
coll insert Trophy.make(userId, kind) void
def award(userId: String, kind: Trophy.Kind.type => Trophy.Kind): Funit =
award(userId, kind(Trophy.Kind))
def awardMarathonWinner(userId: String): Funit = award(userId, Trophy.Kind.MarathonWinner)
final class TrophyApi(coll: Coll, kindColl: Coll) {
private implicit val trophyBSONHandler = Macros.handler[SimplifiedTrophy]
private implicit val trophyKindBSONHandler = Macros.handler[TrophyKind]
def findByUser(user: User, max: Int = 50): Fu[List[Trophy]] =
coll.find($doc("user" -> user.id)).list[Trophy](max)
coll.find($doc("user" -> user.id)).list[SimplifiedTrophy](max) flatMap { l =>
unsimplifyList(l)
}
def roleBasedTrophies(user: User, isPublicMod: Boolean, isDev: Boolean, isVerified: Boolean): Fu[List[Trophy]] = List(
isPublicMod option unsimplify(SimplifiedTrophy(
_id = "",
user = user.id,
kind = TrophyKind.moderator,
date = org.joda.time.DateTime.now
)),
isDev option unsimplify(SimplifiedTrophy(
_id = "",
user = user.id,
kind = TrophyKind.developer,
date = org.joda.time.DateTime.now
)),
isVerified option unsimplify(SimplifiedTrophy(
_id = "",
user = user.id,
kind = TrophyKind.verified,
date = org.joda.time.DateTime.now
))
).flatten.sequenceFu.map(_.flatten)
def unsimplifyList(simplified: List[SimplifiedTrophy]): Fu[List[Trophy]] =
simplified.map({ t =>
unsimplify(t)
}).sequenceFu.map(_.flatten)
def unsimplify(simplified: SimplifiedTrophy): Fu[Option[Trophy]] =
kindColl.byId[TrophyKind](simplified.kind) map2 { (kind: TrophyKind) =>
Trophy(
_id = simplified._id,
user = simplified.user,
kind = kind,
date = simplified.date
)
}
def award(userId: String, kindKey: String): Funit =
coll insert SimplifiedTrophy.make(userId, kindKey) void
}