Skip to content

Commit 247a08e

Browse files
Leguuzsotroav
andauthored
Add minigame command (#23)
* Create a base * add multi channel support * cleaning up * removed JDA entities * replaced String IDs with Long IDs where possible * fixed delay, added reactionRetr check, and few other small tweaks * changed 'U+' notation to the other one * fixed custom reaction error message * fix small grammar error Co-authored-by: zsotroav <49715074+zsotroav@users.noreply.github.com>
1 parent b3f2a79 commit 247a08e

File tree

5 files changed

+646
-0
lines changed

5 files changed

+646
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Open source bot built by and for the Camp Buddy Discord Fan Server.
3+
* Copyright (C) 2020 Kyuuto-devs
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package io.github.yuutoproject.yuutobot.commands
20+
21+
import com.fasterxml.jackson.core.type.TypeReference
22+
import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand
23+
import io.github.yuutoproject.yuutobot.commands.base.CommandCategory
24+
import io.github.yuutoproject.yuutobot.commands.minigame.MinigameInstance
25+
import io.github.yuutoproject.yuutobot.commands.minigame.MinigameListener
26+
import io.github.yuutoproject.yuutobot.objects.Question
27+
import io.github.yuutoproject.yuutobot.utils.jackson
28+
import kotlinx.coroutines.GlobalScope
29+
import kotlinx.coroutines.launch
30+
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent
31+
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent
32+
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent
33+
34+
class Minigame : AbstractCommand(
35+
"minigame",
36+
CommandCategory.FUN,
37+
"Play a fun quiz with your friends!",
38+
"Run `minigame` to begin a new game, and react within the countdown to join.\nRun `minigame skip` to skip a question you do not wish to answer."
39+
) {
40+
// String is the channel ID
41+
private var minigames = mutableMapOf<Long, MinigameInstance>()
42+
private val listener = MinigameListener(this)
43+
private val questions: List<Question>
44+
45+
init {
46+
val json = jackson.readTree(this.javaClass.getResource("/minigame.json"))
47+
questions = jackson.readValue(json.traverse(), object : TypeReference<List<Question>>() {})
48+
}
49+
50+
override fun run(args: MutableList<String>, event: GuildMessageReceivedEvent) {
51+
val id = event.channel.idLong
52+
53+
// Handling for when a game is already in progress
54+
val minigame = minigames[id]
55+
56+
if (minigame != null) {
57+
// If a user attempts to skip a question
58+
if (args.getOrNull(0) == "skip") {
59+
if (!minigame.players.contains(event.author.idLong)) {
60+
event.channel.sendMessage("You can't skip a question if you aren't in the game!").queue()
61+
return
62+
}
63+
64+
if (!minigame.begun) {
65+
event.channel.sendMessage("The game has not started yet!").queue()
66+
return
67+
}
68+
69+
event.channel.sendMessage("Skipping question...").queue()
70+
minigame.progress(event)
71+
return
72+
}
73+
74+
// If the user attempts to start a new game while a game is already in progress,
75+
// Either cancel it if it's stale or indicate that a game is already in progress
76+
if (System.currentTimeMillis() - minigame.timer > 30_000) {
77+
event.channel.sendMessage("Cancelling stale game...").queue()
78+
unregister(minigame)
79+
// Continue outside of the if block and create a new game instance...
80+
} else {
81+
event.channel.sendMessage("A game is already running!").queue()
82+
return
83+
}
84+
}
85+
86+
// Register our listener if it doesn't already exist
87+
if (!event.jda.registeredListeners.contains(listener)) {
88+
event.jda.addEventListener(listener)
89+
}
90+
91+
val maxRounds = args.getOrNull(0)?.toIntOrNull() ?: 7
92+
if (maxRounds < 2 || maxRounds > 10) {
93+
event.channel.sendMessage("The number of rounds has to be greater than 1 and less than 11.").queue()
94+
return
95+
}
96+
97+
minigames[id] = MinigameInstance(
98+
questions.shuffled().toMutableList(),
99+
event.channel.idLong,
100+
this,
101+
maxRounds
102+
)
103+
104+
// Launch the game instance.
105+
GlobalScope.launch {
106+
minigames[id]!!.start(event)
107+
}
108+
}
109+
110+
// Used by MinigameInstances to indicate that they're done
111+
fun unregister(minigame: MinigameInstance) {
112+
minigames.remove(minigame.id)
113+
}
114+
115+
fun messageRecv(event: GuildMessageReceivedEvent) {
116+
val minigame = minigames[event.channel.idLong] ?: return
117+
118+
if (!minigame.begun || !minigame.players.contains(event.author.idLong)) return
119+
120+
minigame.answerReceived(event)
121+
}
122+
123+
fun reactionRecv(event: GuildMessageReactionAddEvent) {
124+
minigames[event.channel.idLong]?.reactionRecv(event)
125+
}
126+
127+
fun reactionRetr(event: GuildMessageReactionRemoveEvent) {
128+
minigames[event.channel.idLong]?.reactionRetr(event)
129+
}
130+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Open source bot built by and for the Camp Buddy Discord Fan Server.
3+
* Copyright (C) 2020 Kyuuto-devs
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package io.github.yuutoproject.yuutobot.commands.minigame
20+
21+
import io.github.yuutoproject.yuutobot.commands.Minigame
22+
import io.github.yuutoproject.yuutobot.objects.Question
23+
import kotlinx.coroutines.delay
24+
import net.dv8tion.jda.api.EmbedBuilder
25+
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent
26+
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent
27+
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent
28+
29+
class MinigameInstance(
30+
private val questions: MutableList<Question>,
31+
// The ID of the channel the game is in
32+
// Effectively the ID of the game instance itself
33+
val id: Long,
34+
// This is necessary for when the game is finished and we want to remove the object
35+
private val manager: Minigame,
36+
private val maxRounds: Int
37+
) {
38+
// Is the game in progress or not?
39+
var begun = false
40+
41+
// String ID of user and their scores
42+
var players = mutableMapOf<Long, Int>()
43+
var timer = System.currentTimeMillis()
44+
45+
private var rounds = 1
46+
47+
private lateinit var startingMessageID: String
48+
49+
private lateinit var currentQuestion: Question
50+
private lateinit var currentAnswers: MutableList<String>
51+
52+
suspend fun start(event: GuildMessageReceivedEvent) {
53+
event.channel.sendMessage("Starting a game with $maxRounds rounds...").queue()
54+
55+
val embed = EmbedBuilder()
56+
.setColor(0xFF93CE)
57+
.setTitle("Minigame Starting!")
58+
.setDescription(
59+
"React below to join the game! \n" +
60+
"This game may contain spoilers or NSFW themes.\n" +
61+
"Please run `minigame skip` in order to skip a question."
62+
)
63+
val startingMessage = event.channel.sendMessage(embed.build()).complete()
64+
startingMessageID = startingMessage.id
65+
66+
startingMessage.addReaction("\uD83C\uDDF4").complete()
67+
68+
for (countdown in 10 downTo 0 step 2) {
69+
embed.setDescription(
70+
"React below to join the game!\n" +
71+
"This game may contain spoilers or NSFW themes.\n" +
72+
"Please run `minigame skip` in order to skip a question.\n" +
73+
"Current players: ${getPlayers()}\n" +
74+
"$countdown seconds left!"
75+
)
76+
startingMessage.editMessage(embed.build()).complete()
77+
78+
delay(2000)
79+
}
80+
81+
if (players.isEmpty()) {
82+
embed.setTitle("Minigame cancelled!").setDescription("Nobody joined...")
83+
startingMessage.editMessage(embed.build()).complete()
84+
manager.unregister(this)
85+
return
86+
}
87+
88+
embed.setTitle("Minigame started!").setDescription("The game has begun!")
89+
startingMessage.editMessage(embed.build()).complete()
90+
91+
begun = true
92+
progress(event)
93+
}
94+
95+
fun progress(event: GuildMessageReceivedEvent) {
96+
if (rounds > maxRounds) {
97+
endGame(event)
98+
return
99+
}
100+
101+
// Unfortunately, no removeOrNull, so we have to use a try/catch
102+
try {
103+
currentQuestion = questions.removeAt(0)
104+
} catch (e: IndexOutOfBoundsException) {
105+
endGame(event)
106+
return
107+
}
108+
109+
currentAnswers = currentQuestion.answers.map { it.toLowerCase() }
110+
.toMutableList()
111+
112+
if (currentQuestion.type == "FILL") {
113+
event.channel.sendMessage(currentQuestion.question).queue()
114+
} else if (currentQuestion.type == "MULTIPLE") {
115+
val questionString = "${currentQuestion.question}\n"
116+
117+
val answerString = (currentQuestion.wrong + currentQuestion.answers).shuffled()
118+
.mapIndexed { i, answer ->
119+
if (currentAnswers.contains(answer.toLowerCase())) {
120+
currentAnswers.add((i + 1).toString())
121+
}
122+
123+
"${i + 1}) $answer"
124+
}.joinToString("\n")
125+
126+
event.channel.sendMessage(questionString + answerString).queue()
127+
}
128+
}
129+
130+
private fun endGame(event: GuildMessageReceivedEvent) {
131+
// Sort by descending value and then map each value to a line in the scoreboard, then join it
132+
val scoreboard = players.entries.sortedByDescending { it.value }.mapIndexed { i, entry ->
133+
"${i + 1}) <@${entry.key}> with ${entry.value} points"
134+
}.joinToString("\n")
135+
136+
val embed = EmbedBuilder()
137+
.setColor(0xFF93CE)
138+
.setTitle("Minigame ended!")
139+
.setDescription("Total points:\n$scoreboard")
140+
event.channel.sendMessage(embed.build()).queue()
141+
142+
manager.unregister(this)
143+
}
144+
145+
fun answerReceived(event: GuildMessageReceivedEvent) {
146+
// A new guess is made, so we reset the stale-game timer
147+
timer = System.currentTimeMillis()
148+
149+
if (currentAnswers.contains(event.message.contentStripped.toLowerCase())) {
150+
players[event.author.idLong] = players[event.author.idLong]!! + 1
151+
event.channel.sendMessage("<@${event.author.id}> got the point!").queue()
152+
rounds += 1
153+
progress(event)
154+
}
155+
}
156+
157+
fun reactionRecv(event: GuildMessageReactionAddEvent) {
158+
if (
159+
event.user.isBot ||
160+
players.contains(event.user.idLong) ||
161+
begun ||
162+
event.messageId != startingMessageID ||
163+
!event.reactionEmote.isEmoji ||
164+
event.reactionEmote.emoji != "\uD83C\uDDF4"
165+
) return
166+
167+
players[event.user.idLong] = 0
168+
}
169+
170+
fun reactionRetr(event: GuildMessageReactionRemoveEvent) {
171+
if (
172+
event.messageId == startingMessageID &&
173+
event.reactionEmote.isEmoji &&
174+
event.reactionEmote.emoji == "\uD83C\uDDF4" &&
175+
!begun
176+
) {
177+
players.remove(event.user!!.idLong)
178+
}
179+
}
180+
181+
private fun getPlayers() = if (players.isNotEmpty()) {
182+
players.keys.joinToString(", ") { "<@$it>" }
183+
} else {
184+
"none"
185+
}
186+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Open source bot built by and for the Camp Buddy Discord Fan Server.
3+
* Copyright (C) 2020 Kyuuto-devs
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package io.github.yuutoproject.yuutobot.commands.minigame
20+
21+
import io.github.yuutoproject.yuutobot.commands.Minigame
22+
import net.dv8tion.jda.api.events.GenericEvent
23+
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent
24+
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent
25+
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent
26+
import net.dv8tion.jda.api.hooks.EventListener
27+
28+
class MinigameListener(private val minigame: Minigame) : EventListener {
29+
override fun onEvent(event: GenericEvent) {
30+
when (event) {
31+
is GuildMessageReceivedEvent -> minigame.messageRecv(event)
32+
is GuildMessageReactionAddEvent -> minigame.reactionRecv(event)
33+
is GuildMessageReactionRemoveEvent -> minigame.reactionRetr(event)
34+
}
35+
}
36+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Open source bot built by and for the Camp Buddy Discord Fan Server.
3+
* Copyright (C) 2020 Kyuuto-devs
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package io.github.yuutoproject.yuutobot.objects
20+
21+
import com.fasterxml.jackson.annotation.JsonCreator
22+
import com.fasterxml.jackson.annotation.JsonProperty
23+
24+
class Question @JsonCreator constructor(
25+
@JsonProperty("type") val type: String,
26+
@JsonProperty("question") val question: String,
27+
@JsonProperty("answers") val answers: MutableList<String>,
28+
@JsonProperty("wrong") val wrong: List<String>
29+
)

0 commit comments

Comments
 (0)