diff --git a/build.gradle.kts b/build.gradle.kts index 99f71d6..7063c9d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,7 @@ plugins { repositories { mavenCentral() + maven { url = uri("https://jitpack.io") } } group = "de.bigboot.ggtools" @@ -81,6 +82,9 @@ dependencies { // CopyDown implementation("io.github.furstenheim:copy_down:1.1") + // Glicko2 + implementation("io.github.gorgtopalski:glicko2-team:3666e16") + // JUnit testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.1") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.1") diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/Config.kt b/src/main/kotlin/de/bigboot/ggtools/fang/Config.kt index a523ecf..dde96ba 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/Config.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/Config.kt @@ -13,6 +13,7 @@ data class EmojisConfig( val deny: String = "\uD83D\uDC4E", val match_finished: String = "\uD83C\uDFC1", val match_drop: String = "\uD83D\uDC4E", + val match_unranked: String = "\uD83C\uDFC5", val queue_empty: String = "\uD83D\uDE22", val join_queue: String = "\uD83D\uDC4D", val leave_queue: String = "\uD83D\uDC4E", @@ -43,13 +44,14 @@ data class BotConfig( val token: String, val prefix: String = "!", val accept_timeout: Int = 120, - val mapvote_time: Int = 30, + val vote_time: Int = 30, val statusupdate_poll_rate: Long = 2000L, val required_players: Int = 10, val log_level: String = "info", val queues: List = listOf(), val highscore_channel: String = "", val time_to_join: Int = 600, + val rating: Boolean = true, ) @JsonClass(generateAdapter = true) diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/ServerApi.kt b/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/ServerApi.kt index 0e1dd9d..ac7cafb 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/ServerApi.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/ServerApi.kt @@ -24,4 +24,7 @@ interface ServerApi { @POST("events") suspend fun getEvents(@Body eventsRequest: EventsRequest): EventsResponse + + @POST("result") + suspend fun getResult(@Body resultRequest: ResultRequest): ResultResponse } diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/model/ResultRequest.kt b/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/model/ResultRequest.kt new file mode 100644 index 0000000..dda6a4c --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/model/ResultRequest.kt @@ -0,0 +1,10 @@ +package de.bigboot.ggtools.fang.api.agent.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ResultRequest( + @field:Json(name = "id") + val id: Int +) diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/model/ResultResponse.kt b/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/model/ResultResponse.kt new file mode 100644 index 0000000..2dd226b --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/api/agent/model/ResultResponse.kt @@ -0,0 +1,10 @@ +package de.bigboot.ggtools.fang.api.agent.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ResultResponse( + @field:Json(name = "winner") + val winner: String? +) diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/commands/Root.kt b/src/main/kotlin/de/bigboot/ggtools/fang/commands/Root.kt index 6a11311..038dedb 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/commands/Root.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/commands/Root.kt @@ -5,6 +5,7 @@ import de.bigboot.ggtools.fang.CommandGroupSpec import de.bigboot.ggtools.fang.commands.admin.Admin import de.bigboot.ggtools.fang.commands.queue.Queue import de.bigboot.ggtools.fang.commands.server.Server +import de.bigboot.ggtools.fang.commands.rating.Rating import de.bigboot.ggtools.fang.utils.createEmbedCompat import de.bigboot.ggtools.fang.utils.formatCommandHelp import de.bigboot.ggtools.fang.utils.formatCommandTree @@ -15,6 +16,7 @@ class Root : CommandGroupSpec("", "") { group(Admin()) group(Queue()) group(Server()) + group(Rating()) command("help", "show this help") { onCall { diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/commands/rating/Rating.kt b/src/main/kotlin/de/bigboot/ggtools/fang/commands/rating/Rating.kt new file mode 100644 index 0000000..14e30d9 --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/commands/rating/Rating.kt @@ -0,0 +1,65 @@ +package de.bigboot.ggtools.fang.commands.rating + +import de.bigboot.ggtools.fang.CommandGroupBuilder +import de.bigboot.ggtools.fang.CommandGroupSpec +import de.bigboot.ggtools.fang.Config +import de.bigboot.ggtools.fang.service.RatingService +import de.bigboot.ggtools.fang.utils.* +import discord4j.common.util.Snowflake +import kotlinx.coroutines.reactive.awaitSingle +import okhttp3.Request +import okhttp3.OkHttpClient +import org.koin.core.component.inject + +class Rating : CommandGroupSpec("rating", "Commands for ratings") { + private val ratingService by inject() + + override val build: CommandGroupBuilder.() -> Unit = { + command("import", "import many games into the rating system") { + onCall { + val attachments = message.attachments; + if (!attachments.isEmpty()) { + var client = OkHttpClient(); + val message = channel().createMessageCompat { + addEmbedCompat { + description("Adding scores") + } + }.awaitSingle() + attachments.forEach { + val request = Request.Builder().url(it.url).build(); + val response = client.newCall(request).execute().body(); + if (response != null) { + response.string().lines().forEach { + if (it.trim() != "") { + val split = it.split(" ").map{it.toLong()}; + val half = split.size/2; + ratingService.addResult(split.slice(0..half-1), split.slice(half..split.size-1)); + } + } + } + else { + channel().createMessageCompat { + addEmbedCompat { + description("Failed to read the contents of your attachment, please try in a few minuets") + } + }.awaitSingle() + return@onCall + } + } + message.editCompat { + addEmbedCompat { + description("Added all of the results!") + } + }.awaitSingle() + } + else { + channel().createMessageCompat { + addEmbedCompat { + description("No attachments were provided") + } + }.awaitSingle() + } + } + } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonAccept.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonAccept.kt index 298c89e..0a84882 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonAccept.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonAccept.kt @@ -1,16 +1,17 @@ package de.bigboot.ggtools.fang.components.queue +import discord4j.common.util.Snowflake import discord4j.core.`object`.component.ActionComponent import discord4j.core.`object`.component.Button import java.util.* -data class ButtonAccept(val matchId: UUID): QueueComponentSpec { - override fun id() = "$PREFIX:${matchId}" +data class ButtonAccept(val matchId: UUID, val dropper: Snowflake?): QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}:${if (dropper != null) dropper.asLong() else ""}" override fun component(): ActionComponent = Button.success(id(), "Accept") companion object { private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:BUTTON:ACCEPT" - private val ID_REGEX = Regex("$PREFIX:([^:]+)") - fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId) -> ButtonAccept(UUID.fromString(matchId)) } + private val ID_REGEX = Regex("$PREFIX:([^:]+):([^:]+)?") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId, dropper) -> ButtonAccept(UUID.fromString(matchId), if (dropper != "") Snowflake.of(dropper) else null) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonDownvote.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonDownvote.kt new file mode 100644 index 0000000..eccdf9f --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonDownvote.kt @@ -0,0 +1,18 @@ +package de.bigboot.ggtools.fang.components.queue + +import discord4j.common.util.Snowflake +import discord4j.core.`object`.component.ActionComponent +import discord4j.core.`object`.component.Button +import java.util.* +import de.bigboot.ggtools.fang.utils.asReaction + +data class ButtonDownvote(val matchId: UUID, val suggester: Snowflake): QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}:${suggester.asLong()}" + override fun component(): ActionComponent = Button.danger(id(), "👎".asReaction(), "") + + companion object { + private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:BUTTON:DOWNVOTE" + private val ID_REGEX = Regex("$PREFIX:([^:]+):([^:]+)") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId, suggester) -> ButtonDownvote(UUID.fromString(matchId), Snowflake.of(suggester) ) } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonMatchUnranked.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonMatchUnranked.kt new file mode 100644 index 0000000..876346d --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonMatchUnranked.kt @@ -0,0 +1,18 @@ +package de.bigboot.ggtools.fang.components.queue + +import de.bigboot.ggtools.fang.Config +import de.bigboot.ggtools.fang.utils.asReaction +import discord4j.core.`object`.component.ActionComponent +import discord4j.core.`object`.component.Button +import java.util.UUID + +data class ButtonMatchUnranked(val matchId: UUID, val final: Boolean): QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}:${final}" + override fun component(): ActionComponent = Button.primary(id(), Config.emojis.match_unranked.asReaction(), "Set unranked") + + companion object { + private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:BUTTON:MATCH_UNRANKED" + private val ID_REGEX = Regex("$PREFIX:([^:]+):(true|false)") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId, final) -> ButtonMatchUnranked(UUID.fromString(matchId), final.toBoolean()) } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonRequestFill.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonRequestFill.kt new file mode 100644 index 0000000..8cf646c --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonRequestFill.kt @@ -0,0 +1,18 @@ +package de.bigboot.ggtools.fang.components.queue + +import de.bigboot.ggtools.fang.Config +import de.bigboot.ggtools.fang.utils.asReaction +import discord4j.core.`object`.component.ActionComponent +import discord4j.core.`object`.component.Button +import java.util.UUID + +data class ButtonRequestFill(val matchId: UUID): QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}" + override fun component(): ActionComponent = Button.primary(id(), "Request Fill") + + companion object { + private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:BUTTON:REQUEST_FILL" + private val ID_REGEX = Regex("$PREFIX:([^:]+)") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId) -> ButtonRequestFill(UUID.fromString(matchId)) } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonRequestFillCancel.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonRequestFillCancel.kt new file mode 100644 index 0000000..1b4e744 --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonRequestFillCancel.kt @@ -0,0 +1,19 @@ +package de.bigboot.ggtools.fang.components.queue + +import de.bigboot.ggtools.fang.Config +import de.bigboot.ggtools.fang.utils.asReaction +import discord4j.common.util.Snowflake +import discord4j.core.`object`.component.ActionComponent +import discord4j.core.`object`.component.Button +import java.util.UUID + +data class ButtonRequestFillCancel(val matchId: UUID, val dropper: Snowflake): QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}:${dropper.asLong()}" + override fun component(): ActionComponent = Button.primary(id(), "Cancel") + + companion object { + private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:BUTTON:REQUEST_FILL_CANCEL" + private val ID_REGEX = Regex("$PREFIX:([^:]+):([^:]+)") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId, dropper) -> ButtonRequestFillCancel(UUID.fromString(matchId), Snowflake.of(dropper)) } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonSuggestSwap.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonSuggestSwap.kt new file mode 100644 index 0000000..99af51a --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonSuggestSwap.kt @@ -0,0 +1,18 @@ +package de.bigboot.ggtools.fang.components.queue + +import de.bigboot.ggtools.fang.Config +import de.bigboot.ggtools.fang.utils.asReaction +import discord4j.core.`object`.component.ActionComponent +import discord4j.core.`object`.component.Button +import java.util.UUID + +data class ButtonSuggestSwap(val matchId: UUID, val final: Boolean): QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}:${final}" + override fun component(): ActionComponent = Button.primary(id(), "Suggest Swap") + + companion object { + private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:BUTTON:SUGGEST_SWAP" + private val ID_REGEX = Regex("$PREFIX:([^:]+):([^:]+)") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId, final) -> ButtonSuggestSwap(UUID.fromString(matchId), final.toBoolean()) } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonUpvote.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonUpvote.kt new file mode 100644 index 0000000..620d298 --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/ButtonUpvote.kt @@ -0,0 +1,18 @@ +package de.bigboot.ggtools.fang.components.queue + +import discord4j.common.util.Snowflake +import discord4j.core.`object`.component.ActionComponent +import discord4j.core.`object`.component.Button +import java.util.* +import de.bigboot.ggtools.fang.utils.asReaction + +data class ButtonUpvote(val matchId: UUID, val suggester: Snowflake): QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}:${suggester.asLong()}" + override fun component(): ActionComponent = Button.success(id(), "👍".asReaction(), "") + + companion object { + private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:BUTTON:UPVOTE" + private val ID_REGEX = Regex("$PREFIX:([^:]+):([^:]+)") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId, suggester) -> ButtonUpvote(UUID.fromString(matchId), Snowflake.of(suggester) ) } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/SelectPickSwap.kt b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/SelectPickSwap.kt new file mode 100644 index 0000000..193841b --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/components/queue/SelectPickSwap.kt @@ -0,0 +1,24 @@ +package de.bigboot.ggtools.fang.components.queue + +import discord4j.common.util.Snowflake +import discord4j.core.GatewayDiscordClient +import discord4j.core.`object`.entity.Member +import discord4j.core.`object`.component.ActionComponent +import discord4j.core.`object`.component.SelectMenu +import java.util.* +import de.bigboot.ggtools.fang.utils.awaitSingle + +class SelectPickSwap(val matchId: UUID, private val players: List>, val team: Boolean): + QueueComponentSpec { + override fun id() = "$PREFIX:${matchId}:${team.toString()}" + override fun component(): ActionComponent = SelectMenu.of( + id(), + players.map { SelectMenu.Option.of(it.first, it.second.toString()) } + ).withMinValues(1).withMaxValues(1).withPlaceholder("Pick a person") + + companion object { + private val PREFIX = "${QueueComponentSpec.ID_PREFIX}:SELECT:PICK_SWAP" + private val ID_REGEX = Regex("$PREFIX:([^:]+):([^:]+)") + fun parse(id: String) = ID_REGEX.find(id)?.destructured?.let { (matchId, team) -> SelectPickSwap(UUID.fromString(matchId), listOf(), team.toBoolean()) } + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/db/Users.kt b/src/main/kotlin/de/bigboot/ggtools/fang/db/Users.kt index af79f70..80c7433 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/db/Users.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/db/Users.kt @@ -10,11 +10,9 @@ class User(id: EntityID) : UUIDEntity(id) { companion object : UUIDEntityClass(Users) var snowflake by Users.snowflake - var skill by Users.skill var groups by Group via UsersGroups } object Users : UUIDTable() { val snowflake = long("snowflake") - val skill = integer("skill").default(1) } diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/db/UsersRating.kt b/src/main/kotlin/de/bigboot/ggtools/fang/db/UsersRating.kt new file mode 100644 index 0000000..b95f92f --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/db/UsersRating.kt @@ -0,0 +1,23 @@ +package de.bigboot.ggtools.fang.db + +import org.jetbrains.exposed.dao.UUIDEntity +import org.jetbrains.exposed.dao.UUIDEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.UUIDTable +import java.util.* + +class UserRating(id: EntityID) : UUIDEntity(id) { + companion object : UUIDEntityClass(UsersRating) + + var snowflake by UsersRating.snowflake + var rating by UsersRating.rating + var ratingDeviation by UsersRating.ratingDeviation + var volatility by UsersRating.volatility +} + +object UsersRating : UUIDTable() { + val snowflake = long("snowflake") + val rating = double("rating") + val ratingDeviation = double("ratingDeviation") + val volatility = double("volatility") +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/db/migrations/v11__add_match_making.kt b/src/main/kotlin/de/bigboot/ggtools/fang/db/migrations/v11__add_match_making.kt new file mode 100644 index 0000000..c531194 --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/db/migrations/v11__add_match_making.kt @@ -0,0 +1,27 @@ +@file:Suppress("ClassName", "ClassNaming", "unused", "LongMethod") + +package de.bigboot.ggtools.fang.db.migrations + +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context + +class V11__add_match_making : BaseJavaMigration() { + override fun migrate(context: Context) { + context.connection.prepareStatement(""" + |alter table Users + | drop column skill; + """.trimMargin()).execute() + + context.connection.prepareStatement(""" + |create table if not exists UsersRating + |( + | id binary(16) not null + | primary key, + | snowflake long not null, + | rating double not null, + | ratingDeviation double not null, + | volatility double not null + |); + """.trimMargin()).execute() + } +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/di/serviceModule.kt b/src/main/kotlin/de/bigboot/ggtools/fang/di/serviceModule.kt index 90db579..b69e358 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/di/serviceModule.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/di/serviceModule.kt @@ -12,6 +12,7 @@ val serviceModule = module { single { ServerServiceImpl() } bind ServerService::class single { ChangelogServiceImpl() } bind ChangelogService::class single { PreferencesServiceImpl() } bind PreferencesService::class + single { RatingServiceImpl() } bind RatingService::class single { SetupGuildServiceImpl() } binds arrayOf(AutostartService::class, SetupGuildService::class) single { CommandsService() } bind AutostartService::class diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/service/ChangelogServiceImpl.kt b/src/main/kotlin/de/bigboot/ggtools/fang/service/ChangelogServiceImpl.kt index df5bd2f..0037f4c 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/service/ChangelogServiceImpl.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/service/ChangelogServiceImpl.kt @@ -83,6 +83,12 @@ private val CHANGELOG = listOf( ), listOf( "Add ability to have a time to join" + ), + listOf( + "Added raking system", + "Added matchmaking", + "Add a request a drop button", + "Can make a swap through the bot" ) ) diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchService.kt b/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchService.kt index 53e31b7..d235791 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchService.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchService.kt @@ -6,6 +6,8 @@ interface MatchService { fun leave(queue: String, snowflake: Long, matchOnly: Boolean = false): Boolean + fun setInMatch(queue: String, snowflake: Long) + fun canPop(queue: String): Boolean fun force(queue: String) diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchServiceImpl.kt b/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchServiceImpl.kt index 5622831..9c27e7b 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchServiceImpl.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/service/MatchServiceImpl.kt @@ -52,6 +52,14 @@ class MatchServiceImpl : MatchService, KoinComponent { } } + override fun setInMatch(queue: String, snowflake: Long) { + return transaction { + val player = Player.find { (Players.snowflake eq snowflake) and (Players.queue eq queue) } + .firstOrNull() ?: return@transaction + player.inMatch = true + } + } + override fun canPop(queue: String): Boolean = force.contains(queue) || requests.containsKey(queue) || getNumPlayers(queue) >= Config.bot.required_players diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/service/QueueMessageService.kt b/src/main/kotlin/de/bigboot/ggtools/fang/service/QueueMessageService.kt index 7624ae7..80d12d8 100644 --- a/src/main/kotlin/de/bigboot/ggtools/fang/service/QueueMessageService.kt +++ b/src/main/kotlin/de/bigboot/ggtools/fang/service/QueueMessageService.kt @@ -3,6 +3,8 @@ package de.bigboot.ggtools.fang.service import de.bigboot.ggtools.fang.Config import de.bigboot.ggtools.fang.api.agent.model.StartRequest import de.bigboot.ggtools.fang.api.agent.model.StartResponse +import de.bigboot.ggtools.fang.api.agent.model.ResultRequest +import de.bigboot.ggtools.fang.api.agent.model.ResultResponse import de.bigboot.ggtools.fang.components.queue.QueueComponentSpec import de.bigboot.ggtools.fang.components.queue.* import de.bigboot.ggtools.fang.utils.* @@ -11,6 +13,7 @@ import discord4j.core.GatewayDiscordClient import discord4j.core.event.domain.interaction.ComponentInteractionEvent import discord4j.core.event.domain.interaction.SelectMenuInteractionEvent import discord4j.core.`object`.component.ActionRow +import discord4j.core.`object`.entity.Member import discord4j.core.`object`.entity.Message import discord4j.core.`object`.entity.channel.MessageChannel import discord4j.core.spec.EmbedCreateSpec @@ -38,6 +41,35 @@ enum class MatchState { MATCH_READY, } +enum class DropState { + WAITING, + WAITING_PLAYER, + DONE, +} + +enum class SwapState { + CREATATION, + VOTING, +} + +data class FillRequest( + val matchRequest: UUID, + var message: Message? = null, + var state: DropState = DropState.WAITING, + var filler: Snowflake? = null, +) + +data class SwapRequest( + val matchRequest: UUID, + var message: Message? = null, + var state: SwapState = SwapState.CREATATION, + var teamOne: Snowflake? = null, + var teamTwo: Snowflake? = null, + var upVotes: HashSet = hashSetOf(), + var downVotes: HashSet = hashSetOf(), + var endTime: Instant? = null +) + data class MatchRequest( val queue: String, val popEndTime: Instant, @@ -56,6 +88,10 @@ data class MatchRequest( var creatures: Triple = Triple(null, null, null), var state: MatchState = MatchState.QUEUE_POP, var timeToJoin: Instant? = null, + var teams: Pair, List>? = null, + var ranked: Snowflake? = null, + var drops: HashMap = hashMapOf(), + var swaps: HashMap = hashMapOf(), ) { fun getMapVoteResult() = mapVotes .values @@ -72,6 +108,7 @@ class QueueMessageService : AutostartService, KoinComponent { private val setupGuildService by inject() private val preferencesService by inject() private val serverService by inject() + private val ratingService by inject() private val matchReuests = hashMapOf() @@ -96,6 +133,10 @@ class QueueMessageService : AutostartService, KoinComponent { } private suspend fun updateQueueMessage(queue: String, msg: Message) { + matchReuests.forEach { (uuid, match) -> + match.drops.forEach { (dropper, _) -> fillRequest(uuid, dropper, false) } + } + if (matchService.canPop(queue)) { val channel = client.getChannelById(msg.channelId).awaitSingle() as MessageChannel handleQueuePop(queue, matchService.pop(queue), channel) @@ -138,15 +179,39 @@ class QueueMessageService : AutostartService, KoinComponent { }.awaitSafe() } + private fun getPlayers(request: MatchRequest): Set { + var players = request.pop.allPlayers + request.drops.forEach { + (dropper, fillRequest) -> + if (fillRequest.state == DropState.DONE) { + players -= dropper.asLong() + players += fillRequest.filler!!.asLong() + } + } + + return players + } + private suspend fun updateMatchReadyMessage(matchId: UUID) { val request = matchReuests[matchId] ?: return + if (request.teams == null) { + request.teams = ratingService.makeTeams(getPlayers(request).toList()); + } + if(request.state != MatchState.MATCH_READY) return request.message.editCompat { addEmbedCompat { + content(getPlayers(request).joinToString(" ") { "<@$it>" }) + val components = mutableListOf(ButtonMatchFinished(matchId), ButtonMatchDrop(matchId)) + val componentsSecond = mutableListOf(ButtonRequestFill(matchId), ButtonSuggestSwap(matchId, false)) + + if (Config.bot.rating && request.ranked == null) { + components.add(ButtonMatchUnranked(matchId, false)); + } title("Match ready!") description(""" @@ -165,9 +230,18 @@ class QueueMessageService : AutostartService, KoinComponent { addField("Map", Maps.fromId(request.getMapVoteResult())!!.name, true) + if (Config.bot.rating && request.ranked == null) { + addField("Team Differential", String.format("%.1f%%", ratingService.teamDifferential(request.teams!!)*100-100), true) + } + if(request.serverSetupPlayer != null) { val value = when { - request.openUrl != null -> "`open ${request.openUrl}`\nby <@${request.serverSetupPlayer!!.asLong()}>" + request.openUrl != null -> if (Config.bot.rating && request.ranked == null) { + "`open ${request.openUrl}?team=0`\n`open ${request.openUrl}?team=1`\nby <@${request.serverSetupPlayer!!.asLong()}>" + } + else { + "`open ${request.openUrl}`\nby <@${request.serverSetupPlayer!!.asLong()}>" + } else -> "Being set up by <@${request.serverSetupPlayer!!.asLong()}>" } addField("Server", value, false) @@ -175,16 +249,24 @@ class QueueMessageService : AutostartService, KoinComponent { components.add(ButtonMatchSetupServer(matchId)) } - addField("Players", request.pop.allPlayers.joinToString(" ") { "<@$it>" }, false) - + if (Config.bot.rating && request.ranked == null) { + addField("Team 0", request.teams!!.first.joinToString(" ") { "<@$it>" }, false) + addField("Team 1", request.teams!!.second.joinToString(" ") { "<@$it>" }, false) + } + + if (request.ranked != null) { + addField("Set Unraked by", "<@${request.ranked!!.asLong()}>", false) + } + if(request.timeToJoin != null) { addField("Time to join", when { Instant.now().compareTo(request.timeToJoin) >= 0 -> "If someone is still not in please report them." - else -> "" + else -> "" }, false) } addComponent(ActionRow.of(components.map { it.component() })) + addComponent(ActionRow.of(componentsSecond.map { it.component() })) } }.awaitSingle() } @@ -204,8 +286,8 @@ class QueueMessageService : AutostartService, KoinComponent { } addComponent(ActionRow.of(when(canDeny) { - true -> listOf(ButtonAccept(matchId), ButtonDecline(matchId)) - else -> listOf(ButtonAccept(matchId)) + true -> listOf(ButtonAccept(matchId, null), ButtonDecline(matchId)) + else -> listOf(ButtonAccept(matchId, null)) }.map { it.component() }.toMutableList())) }.awaitSafe() ?: return @@ -236,29 +318,35 @@ class QueueMessageService : AutostartService, KoinComponent { } request.state = MatchState.MAP_VOTE - request.mapVoteEnd = Instant.now().plusSeconds(Config.bot.mapvote_time.toLong()) + request.mapVoteEnd = Instant.now().plusSeconds(Config.bot.vote_time.toLong()) request.message.delete().await() matchReuests[matchId]!!.message = request.message.channel.awaitSingle().createMessageCompat { - content(request.pop.allPlayers.joinToString(" ") { "<@$it>" }) + content(getPlayers(request).joinToString(" ") { "<@$it>" }) }.awaitSingle() - + updateMapVoteMessage(matchId) CoroutineScope(Dispatchers.Default).launch { - delay(Config.bot.mapvote_time.seconds) + delay(Config.bot.vote_time.seconds) handleMapVoteFinished(matchId) } } private suspend fun handleMapVoteFinished(matchId: UUID) { val request = matchReuests[matchId] ?: return - + request.state = MatchState.MATCH_READY updateMatchReadyMessage(matchId) request.message.deleteAfter(90.minutes) { - for (player in request.pop.allPlayers) { + for ((_, drop) in request.drops) { + if (drop.message != null) { + drop.message!!.delete().await() + } + } + + for (player in getPlayers(request)) { matchService.leave(request.queue, player, true) } matchReuests.remove(matchId) @@ -267,7 +355,7 @@ class QueueMessageService : AutostartService, KoinComponent { private suspend fun handleMatchCancelled(request: MatchRequest) { val message = request.message - val accepted = request.pop.allPlayers - request.missingPlayers + val accepted = getPlayers(request) - request.missingPlayers for (player in request.missingPlayers) { matchService.leave(request.queue, player) @@ -297,7 +385,7 @@ class QueueMessageService : AutostartService, KoinComponent { } private suspend fun handleMatchFinished(request: MatchRequest) { - for (player in request.pop.allPlayers - request.dropPlayers) { + for (player in getPlayers(request) - request.dropPlayers) { matchService.join(request.queue, player, true) } @@ -315,6 +403,12 @@ class QueueMessageService : AutostartService, KoinComponent { request.message.deleteAfter(60.seconds) + for ((_, drop) in request.drops) { + if (drop.message != null) { + drop.message!!.delete().await() + } + } + updateQueueMessage(request.queue) } @@ -405,6 +499,131 @@ class QueueMessageService : AutostartService, KoinComponent { )) } + private suspend fun acceptQueue(event: ComponentInteractionEvent, button: ButtonAccept) { + event.deferEdit().awaitSafe() + + val request = matchReuests[button.matchId] ?: return + request.missingPlayers -= event.interaction.user.id.asLong() + + + if(request.missingPlayers.isEmpty()) { + request.matchReady.complete(null) + } else { + event.editReplyCompat { + addEmbedCompat { + printQueuePop(request, this) + } + }.awaitSafe() + } + } + + private suspend fun acceptFill(event: ComponentInteractionEvent, button: ButtonAccept) { + event.deferEdit().awaitSafe() + + val request = matchReuests[button.matchId] ?: return + val dropper = button.dropper ?: return + val filler = event.interaction.user.id.asLong() + val fill = request.drops[dropper] ?: return + val fillFiller = fill.filler ?: return + + if (filler != fillFiller.asLong()) { + return + } + + fill.state = DropState.DONE + fill.message!!.delete().await() + request.teams = null + updateMatchReadyMessage(button.matchId) + } + + private suspend fun fillRequest(matchId: UUID, dropper: Snowflake, timeout: Boolean) { + val request = matchReuests[matchId] ?: return + val channel = request.message.channel.awaitSingle(); + + var fill = request.drops[dropper] ?: FillRequest(matchId) + + if (fill.state == DropState.DONE || (fill.state == DropState.WAITING_PLAYER && !timeout)) { + return + } + var message = fill.message; + + if(matchService.getNumPlayers(request.queue, request.pop.server) >= 1) { + if (fill.state == DropState.WAITING) { + if (message != null) { + message.delete().await() + } + + fill.state = DropState.WAITING_PLAYER + + val newPlayer = matchService.pop(request.queue, request.pop.server, setOf()).players.first(); + val endTime = Instant.now().plusSeconds(Config.bot.accept_timeout.toLong()) + + notifyPlayer(Snowflake.of(newPlayer), channel.id) + + fill.message = channel.createMessageCompat { + content("<@${newPlayer}>") + + addEmbedCompat { + title("Fill Request") + description("<@${dropper.asLong()}> would like to drop, press the Accept button to take the spot.") + addField("Time remaining", "", true) + } + + addComponent(ActionRow.of(ButtonAccept(matchId, dropper).component())) + }.awaitSingle() + + fill.filler = Snowflake.of(newPlayer) + + matchService.setInMatch(request.queue, newPlayer) + updateQueueMessage(request.queue) + + val fillReady = CompletableFuture() + + fillReady.completeOnTimeout(null, Config.bot.accept_timeout.toLong(), TimeUnit.SECONDS) + + CoroutineScope(Dispatchers.Default).launch { + fillReady.await() + if (fill.state == DropState.WAITING_PLAYER) { + matchService.leave(request.queue, newPlayer, true) + } + fillRequest(matchId, dropper, true) + } + } + } else { + if (fill.state == DropState.WAITING_PLAYER && message != null) { + message.delete().await() + } + fill.state = DropState.WAITING + + fill.message = channel.createMessageCompat { + addEmbedCompat { + title("Fill Request") + description("No one is in the queue to fill for <@${dropper.asLong()}>, please wait for someone to join queue or press cancle") + } + + addComponent(ActionRow.of(ButtonRequestFillCancel(matchId, dropper).component())) + }.awaitSingle() + } + + request.drops[dropper] = fill + } + + private suspend fun updateSwapRequest(request: MatchRequest, suggester: Snowflake) { + val swap = request.swaps[suggester] ?: return + + swap.message!!.editCompat { + addEmbedCompat { + title("Swap Request") + description("<@${suggester.asLong()}> has requested a swap, <@${swap.teamOne!!.asLong()}> for <@${swap.teamTwo!!.asLong()}>") + addField("Up votes", swap.upVotes.size.toString(), true) + addField("Down votes", swap.downVotes.size.toString(), true) + + addField("Time remaining", "", false) + } + }.awaitSingle() + + } + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonJoin) { event.deferEdit().awaitSafe() matchService.join(button.queue, event.interaction.user.id.asLong()) @@ -476,20 +695,11 @@ class QueueMessageService : AutostartService, KoinComponent { } private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonAccept) { - event.deferEdit().awaitSafe() - - val request = matchReuests[button.matchId] ?: return - request.missingPlayers -= event.interaction.user.id.asLong() - - - if(request.missingPlayers.isEmpty()) { - request.matchReady.complete(null) - } else { - event.editReplyCompat { - addEmbedCompat { - printQueuePop(request, this) - } - }.awaitSafe() + if (button.dropper == null) { + acceptQueue(event, button) + } + else { + acceptFill(event, button) } } @@ -515,7 +725,7 @@ class QueueMessageService : AutostartService, KoinComponent { event.deferEdit().awaitSafe() val request = matchReuests[button.matchId] ?: return - if(request.pop.allPlayers.contains(event.interaction.user.id.asLong())) { + if(getPlayers(request).contains(event.interaction.user.id.asLong())) { request.mapVotes += Pair(event.interaction.user.id.asLong(), button.map) updateMapVoteMessage(button.matchId) } @@ -525,7 +735,7 @@ class QueueMessageService : AutostartService, KoinComponent { event.deferEdit().awaitSafe() val request = matchReuests[button.matchId] ?: return - if(request.pop.allPlayers.contains(event.interaction.user.id.asLong())) + if(getPlayers(request).contains(event.interaction.user.id.asLong())) { request.dropPlayers += event.interaction.user.id.asLong() } @@ -535,10 +745,37 @@ class QueueMessageService : AutostartService, KoinComponent { event.deferEdit().awaitSafe() val request = matchReuests[button.matchId] ?: return - if(request.pop.allPlayers.contains(event.interaction.user.id.asLong())) { + + if(getPlayers(request).contains(event.interaction.user.id.asLong())) { request.finishedPlayers += event.interaction.user.id.asLong() if(request.finishedPlayers.size >= 2) { + if (Config.bot.rating && request.ranked == null) { + val server = request.server ?: return + + val api = serverService.getClient(server) ?: return + + val result = try { + api.getResult( + ResultRequest( + id = request.openUrl!!.split(":").last().toInt(), + ) + ) + } catch (ex: Exception) { + ResultResponse(null) + } + + result.winner.let { + + if (it == "GRIFFIN") { + ratingService.addResult(request.teams!!.first, request.teams!!.second); + } + else { + ratingService.addResult(request.teams!!.second, request.teams!!.first); + } + } + } + handleMatchFinished(request) matchReuests.remove(button.matchId) } @@ -558,7 +795,7 @@ class QueueMessageService : AutostartService, KoinComponent { event .deferReply(InteractionCallbackSpec.builder().ephemeral(true).build()) .awaitSafe() - + updateMatchReadyMessage(button.matchId) event.editReplyCompat { @@ -566,6 +803,252 @@ class QueueMessageService : AutostartService, KoinComponent { }.awaitSafe() } + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonMatchUnranked) { + val request = matchReuests[button.matchId] + + if(request == null || request.ranked != null) { + event.deferEdit().awaitSafe() + return + } + + if (!button.final) { + event + .deferReply(InteractionCallbackSpec.builder().ephemeral(true).build()) + .awaitSafe() + + updateMatchReadyMessage(button.matchId) + + event.editReplyCompat { + addEmbedCompat { + description("Are you sure you want to set it to unranked? Ill intent use of this will get you reported? If you believe this should be unranked press the set ranked button under this, if not dismiss this message.") + } + addComponent(ActionRow.of(ButtonMatchUnranked(button.matchId, true).component())) + }.awaitSafe() + } + else { + event.deferEdit().withEphemeral(true).awaitSafe() + event.editReplyCompat { + addEmbedCompat { + description("You have set this match to be unranked, you can dissmis this message.") + } + addAllComponents(emptyList()) + }.awaitSafe() + + request.ranked = event.interaction.user.id + + updateMatchReadyMessage(button.matchId) + } + } + + private suspend fun handleInteraction(event: ComponentInteractionEvent, select: SelectPickSwap) { + val matchId = select.matchId; + val suggester = event.interaction.user.id + + val request = matchReuests[matchId] ?: return + val swap = request.swaps[suggester] + val value = Snowflake.of((event as SelectMenuInteractionEvent).values.first()) + + if (swap != null) { + if (select.team == false) { + swap.teamOne = value + } + else { + swap.teamTwo = value + } + } + event.deferEdit().awaitSafe() + } + + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonSuggestSwap) { + val matchId = button.matchId; + val suggester = event.interaction.user.id + val guild = event.interaction.guild.awaitSingle().id + val channel = event.interaction.channel.awaitSingle() + + val request = matchReuests[matchId] ?: return + var swap = request.swaps[suggester] + + if (!button.final) { + if(getPlayers(request).contains(event.interaction.user.id.asLong()) && (swap == null || swap.state == SwapState.CREATATION)) { + request.swaps[suggester] = SwapRequest(matchId) + + val (teamOne, teamTwo) = request.teams!!.toList().map { + it.map { + Pair( + client.getMemberById( + guild, + Snowflake.of(it) + ).await()!!.displayName, + it + ) + } + } + + event + .deferReply(InteractionCallbackSpec.builder().ephemeral(true).build()) + .awaitSafe() + event.editReplyCompat { + addEmbedCompat { + description("Please select the swaps you would like.") + } + addAllComponents(listOf( + ActionRow.of( + SelectPickSwap( + matchId, + teamOne, + false, + ).component()), + ActionRow.of( + SelectPickSwap( + matchId, + teamTwo, + true, + ).component()), + ActionRow.of(ButtonSuggestSwap(matchId, true).component()), + )) + }.awaitSafe() + } + else if (swap != null) { + event + .deferReply(InteractionCallbackSpec.builder().ephemeral(true).build()) + .awaitSafe() + event.editReplyCompat { + addEmbedCompat { + description("Please wait for your other request to finish before making another") + } + }.awaitSafe() + } + else { + event.deferEdit().awaitSafe() + } + } + else { + swap = swap!! + + if (swap.teamTwo == null || swap.teamOne == null) { + return + } + + event.deferEdit().awaitSafe() + event.editReplyCompat { + addEmbedCompat { + description("You can dismiss this message") + } + + addAllComponents(emptyList()) + }.awaitSafe() + + swap.state = SwapState.VOTING + + swap.endTime = Instant.now().plusSeconds(Config.bot.vote_time.toLong()) + + swap.message = channel.createMessageCompat { + content(getPlayers(request).joinToString(" ") { "<@$it>" }) + + addComponent(ActionRow.of(ButtonUpvote(matchId, suggester).component(), ButtonDownvote(matchId, suggester).component())) + }.awaitSingle() + + updateSwapRequest(request, suggester) + + CoroutineScope(Dispatchers.Default).launch { + delay(Config.bot.vote_time.seconds) + if (swap.upVotes.count() > swap.downVotes.count()) { + swap.message!!.editCompat { + addEmbedCompat { + title("Swap Succsess") + description("<@${suggester.asLong()}> requested a swap, <@${swap.teamOne!!.asLong()}> for <@${swap.teamTwo!!.asLong()}>") + } + }.awaitSingle() + + val teamOne = request.teams!!.first.toMutableList() + val teamTwo = request.teams!!.second.toMutableList() + + teamOne.remove(swap.teamOne!!.asLong()) + teamOne.add(swap.teamTwo!!.asLong()) + + teamTwo.add(swap.teamOne!!.asLong()) + teamTwo.remove(swap.teamTwo!!.asLong()) + + request.teams = Pair(teamOne, teamTwo) + + updateMatchReadyMessage(matchId) + } + else { + swap.message!!.editCompat { + addEmbedCompat { + title("Swap Failed") + description("<@${suggester.asLong()}> requested a swap, <@${swap.teamOne!!.asLong()}> for <@${swap.teamTwo!!.asLong()}>") + } + }.awaitSingle() + + swap.message!!.deleteAfter(1.minutes) + } + request.swaps.remove(suggester) + } + } + } + + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonUpvote) { + val request = matchReuests[button.matchId] ?: return + val voter = event.interaction.user.id.asLong() + + if (getPlayers(request).contains(voter)) { + val swap = request.swaps[button.suggester] ?: return + + swap.upVotes.add(voter) + swap.downVotes.remove(voter) + + updateSwapRequest(request, button.suggester) + } + event.deferEdit().awaitSafe() + } + + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonDownvote) { + val request = matchReuests[button.matchId] ?: return + val voter = event.interaction.user.id.asLong() + + if (getPlayers(request).contains(voter)) { + val swap = request.swaps[button.suggester] ?: return + + swap.upVotes.remove(voter) + swap.downVotes.add(voter) + + updateSwapRequest(request, button.suggester) + } + event.deferEdit().awaitSafe() + } + + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonRequestFill) { + val matchId = button.matchId; + val dropper = event.interaction.user.id + + val request = matchReuests[matchId] ?: return + + if(getPlayers(request).contains(event.interaction.user.id.asLong()) && request.drops[dropper] == null) { + fillRequest(matchId, dropper, false); + } + + event.deferEdit().awaitSafe() + } + + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: ButtonRequestFillCancel) { + val request = matchReuests[button.matchId] ?: return + val dropper = event.interaction.user.id + + if (button.dropper != dropper) { + return + } + + var fill = request.drops[dropper] ?: return + + if (fill.message != null) { + fill.message!!.delete().await() + } + + request.drops.remove(dropper) + event.deferEdit().awaitSafe() + } + private suspend fun handleInteraction(event: ComponentInteractionEvent, button: SelectMatchSetupCreatures) { val request = matchReuests[button.matchId] ?: return @@ -604,7 +1087,7 @@ class QueueMessageService : AutostartService, KoinComponent { api.start( StartRequest( map = "lv_${request.getMapVoteResult()}", - maxPlayers = request.pop.allPlayers.size, + maxPlayers = getPlayers(request).size, creature0 = request.creatures.first, creature1 = request.creatures.second, creature2 = request.creatures.third, @@ -636,7 +1119,7 @@ class QueueMessageService : AutostartService, KoinComponent { }.awaitSafe() updateMatchReadyMessage(button.matchId) - + CoroutineScope(Dispatchers.Default).launch { delay(Config.bot.time_to_join.seconds) updateMatchReadyMessage(button.matchId) @@ -656,10 +1139,17 @@ class QueueMessageService : AutostartService, KoinComponent { ButtonMapVote.parse(event.customId)?.also { handleInteraction(event, it); return } ButtonMatchDrop.parse(event.customId)?.also { handleInteraction(event, it); return } ButtonMatchFinished.parse(event.customId)?.also { handleInteraction(event, it); return } + ButtonMatchUnranked.parse(event.customId)?.also { handleInteraction(event, it); return } + ButtonRequestFill.parse(event.customId)?.also { handleInteraction(event, it); return } + ButtonRequestFillCancel.parse(event.customId)?.also { handleInteraction(event, it); return } ButtonMatchSetupServer.parse(event.customId)?.also { handleInteraction(event, it); return } SelectMatchSetupServer.parse(event.customId)?.also { handleInteraction(event, it); return } SelectMatchSetupCreatures.parse(event.customId)?.also { handleInteraction(event, it); return } ButtonMatchStartServer.parse(event.customId)?.also { handleInteraction(event, it); return } + ButtonSuggestSwap.parse(event.customId)?.also { handleInteraction(event, it); return } + ButtonUpvote.parse(event.customId)?.also { handleInteraction(event, it); return } + ButtonDownvote.parse(event.customId)?.also { handleInteraction(event, it); return } + SelectPickSwap.parse(event.customId)?.also { handleInteraction(event, it); return } } private suspend fun notifyPlayer(player: Snowflake, channel: Snowflake) { diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/service/RatingService.kt b/src/main/kotlin/de/bigboot/ggtools/fang/service/RatingService.kt new file mode 100644 index 0000000..fab7e5a --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/service/RatingService.kt @@ -0,0 +1,15 @@ +package de.bigboot.ggtools.fang.service + +import de.bigboot.ggtools.fang.db.UserRating + +interface RatingService { + + fun findUser(snowflake: Long): UserRating? + + fun addResult(winning: List, loosing: List) + + fun makeTeams(players: List): Pair, List> + + fun teamDifferential(teams: Pair, List>): Double + +} diff --git a/src/main/kotlin/de/bigboot/ggtools/fang/service/RatingServiceImpl.kt b/src/main/kotlin/de/bigboot/ggtools/fang/service/RatingServiceImpl.kt new file mode 100644 index 0000000..736fa6d --- /dev/null +++ b/src/main/kotlin/de/bigboot/ggtools/fang/service/RatingServiceImpl.kt @@ -0,0 +1,110 @@ +package de.bigboot.ggtools.fang.service + +import de.bigboot.ggtools.fang.Config +import de.bigboot.ggtools.fang.db.* +import org.goochjs.glicko2.Rating; +import org.goochjs.glicko2.RatingCalculator; +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.util.* +import org.topalski.teams.*; + +class RatingServiceImpl : RatingService, KoinComponent { + private val database: Database by inject() + private val ratingSystem = RatingCalculator(0.06, 0.5); + + override fun findUser(snowflake: Long) = transaction(database) { + return@transaction UserRating.find { UsersRating.snowflake eq snowflake } + .firstOrNull() ?: UserRating.new { + + this.snowflake = snowflake + this.rating = ratingSystem.getDefaultRating() + this.ratingDeviation = ratingSystem.getDefaultRatingDeviation() + this.volatility = ratingSystem.getDefaultVolatility() + }; + } + + override fun addResult(winning: List, loosing: List) = transaction(database) { + var winning = winning.map{findUser(it)}; + var loosing = loosing.map{findUser(it)}; + + var glickoWinning = winning.map{Rating(it.snowflake.toString(), it.rating, it.ratingDeviation, it.volatility)}.toMutableSet(); + var glickoLoosing = loosing.map{Rating(it.snowflake.toString(), it.rating, it.ratingDeviation, it.volatility)}.toMutableSet(); + + var game = TeamIndividualUpdate(); + + var teamWinning = Team(mutableSetOf()); // for some reason when you put glickWinning right into this it does not actually set it + var teamLoosing = Team(mutableSetOf()); + + glickoWinning.forEach { + teamWinning.addTeamPlayer(it) + } + glickoLoosing.forEach { + teamLoosing.addTeamPlayer(it) + } + + game.addResult(teamWinning, teamLoosing); + game.updateRating(ratingSystem); + + var i = 0; + glickoWinning.forEach { + winning[i].rating = it.getRating(); + winning[i].ratingDeviation = it.getRatingDeviation(); + winning[i].volatility = it.getVolatility(); + i++; + } + + i = 0; + glickoLoosing.forEach { + loosing[i].rating = it.getRating(); + loosing[i].ratingDeviation = it.getRatingDeviation(); + loosing[i].volatility = it.getVolatility(); + i++; + } + } + + override fun makeTeams(players: List): Pair, List> { + var ratedTeams = players.map{findUser(it)}; + + var createdTeams: MutableList> = mutableListOf(); + + val halfSize = ratedTeams.size/2; + val halfScore = ratedTeams.map{it.rating}.sum()/2; + + ratedTeams.forEach { + val current = it; + for (i in 0 until createdTeams.size) { + var new = createdTeams[i].toMutableList(); + if (new.size != halfSize && new.map{it.rating}.sum()+current.rating <= halfScore) { + new.add(current); + createdTeams.add(new); + } + } + createdTeams.add(mutableListOf(it)); + } + + val teamOne = createdTeams.filter {it.size == halfSize}.sortedBy {it.map{it.rating}.sum()}.last(); + + var teamTwo = ratedTeams.toMutableList(); + + teamOne.forEach { + teamTwo.remove(it) + } + + return Pair(teamOne.map{it.snowflake}, teamTwo.map{it.snowflake}) + } + + override fun teamDifferential(teams: Pair, List>): Double { + val teamOne = teams.first.map{findUser(it).rating}.sum() + val teamTwo = teams.second.map{findUser(it).rating}.sum() + + if (teamOne > teamTwo) { + return teamOne/teamTwo + } + else { + return teamTwo/teamOne + } + } +}