From f7e706322049256a0e7fced9d831253c3103d4ac Mon Sep 17 00:00:00 2001 From: m1a2st Date: Sun, 8 Oct 2023 19:35:58 +0800 Subject: [PATCH] :sparkles: achievement-system --- .../commons/config/WsaDiscordProperties.kt | 4 + .../gatweay/adapter/MongoCollectionAdapter.kt | 6 +- .../MongoCollection.kt | 2 + utopia-gamification/pom.xml | 5 +- .../application/presenter/Presenter.kt | 7 + .../repository/AchievementRepository.kt | 9 + .../repository/ProgressionRepository.kt | 10 + .../usecase/ProgressAchievementUseCase.kt | 89 ++++++++ .../domain/achievements/Achievement.kt | 87 ++++++++ .../achievements/LongArticleAchievement.kt | 30 +++ .../achievements/TopicMasterAchievement.kt | 24 ++ .../achievement/domain/actions/Action.kt | 22 ++ .../domain/actions/SendMessageAction.kt | 12 + .../domain/events/AchievementAchievedEvent.kt | 7 + .../framework/dao/AchievementDao.kt | 37 ++++ .../framework/dao/ProgressionDao.kt | 35 +++ .../dao/documents/ProgressionDocument.kt | 16 ++ .../framework/listener/AchievementListener.kt | 45 ++++ .../framework/listener/enums/DiscordRole.kt | 26 +++ .../presenter/ProgressAchievementPresenter.kt | 29 +++ .../utopiagamification/quest/domain/Player.kt | 15 +- .../utopiagamification/quest/domain/Quest.kt | 22 +- .../quest/extensions/LevelSheet.kt | 25 ++- .../repositoryimpl/MongodbPlayerRepository.kt | 7 +- .../it/AchievementIntegrationTest.kt | 205 ++++++++++++++++++ .../achievement/ut/AchievementUnitTest.kt | 171 +++++++++++++++ .../configs/MongoDbTestContainerConfig.kt | 4 +- 27 files changed, 919 insertions(+), 32 deletions(-) create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/presenter/Presenter.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/AchievementRepository.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/ProgressionRepository.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/usecase/ProgressAchievementUseCase.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/Achievement.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/LongArticleAchievement.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/TopicMasterAchievement.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/Action.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/SendMessageAction.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/events/AchievementAchievedEvent.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/AchievementDao.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/ProgressionDao.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/documents/ProgressionDocument.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/AchievementListener.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/enums/DiscordRole.kt create mode 100644 utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/presenter/ProgressAchievementPresenter.kt create mode 100644 utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/it/AchievementIntegrationTest.kt create mode 100644 utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/ut/AchievementUnitTest.kt diff --git a/commons/src/main/kotlin/tw/waterballsa/utopia/commons/config/WsaDiscordProperties.kt b/commons/src/main/kotlin/tw/waterballsa/utopia/commons/config/WsaDiscordProperties.kt index 53b31d0b..f658779e 100644 --- a/commons/src/main/kotlin/tw/waterballsa/utopia/commons/config/WsaDiscordProperties.kt +++ b/commons/src/main/kotlin/tw/waterballsa/utopia/commons/config/WsaDiscordProperties.kt @@ -33,6 +33,8 @@ open class WsaDiscordProperties(properties: Properties) { val waterBallJournalPostId: String val waterBallLoseWeightPostId: String val wsaGuideLineChannelId: String + val wsaLongArticleRoleId: String + val wsaTopicMasterRoleId: String init { properties.run { @@ -61,6 +63,8 @@ open class WsaDiscordProperties(properties: Properties) { waterBallJournalPostId = getProperty("water-ball-journal-post-id") waterBallLoseWeightPostId = getProperty("water-ball-lose-weight-post-id") wsaGuideLineChannelId = getProperty("wsa-guideline-channel-id") + wsaLongArticleRoleId = getProperty("wsa-long-article-role-id") + wsaTopicMasterRoleId = getProperty("wsa-topic-master-role-id") } } } diff --git a/mongo-gateway-impl/src/main/kotlin/tw/waterballsa/utopia/mongo/gatweay/adapter/MongoCollectionAdapter.kt b/mongo-gateway-impl/src/main/kotlin/tw/waterballsa/utopia/mongo/gatweay/adapter/MongoCollectionAdapter.kt index e781a4df..6413b51f 100644 --- a/mongo-gateway-impl/src/main/kotlin/tw/waterballsa/utopia/mongo/gatweay/adapter/MongoCollectionAdapter.kt +++ b/mongo-gateway-impl/src/main/kotlin/tw/waterballsa/utopia/mongo/gatweay/adapter/MongoCollectionAdapter.kt @@ -6,8 +6,8 @@ import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.Query import tw.waterballsa.utopia.mongo.gateway.MongoCollection -import tw.waterballsa.utopia.mongo.gateway.Query as MGQuery // Utopia Mongo Gateway Query import tw.waterballsa.utopia.mongo.gatweay.config.MongoDBConfiguration.Companion.MAPPER +import tw.waterballsa.utopia.mongo.gateway.Query as MGQuery private const val MONGO_ID_FIELD_NAME = "_id" @@ -48,6 +48,10 @@ class MongoCollectionAdapter( ).deletedCount } + override fun removeAll() { + mongoTemplate.dropCollection(documentInformation.collectionName) + } + private fun TDocument.toBsonDocument(): Document = MAPPER.convertValue(this, Document::class.java)!! .convertIdField(documentInformation.idFieldName, MONGO_ID_FIELD_NAME) diff --git a/mongo-gateway/src/main/kotlin/tw.waterballsa.utopia.mongo.gateway/MongoCollection.kt b/mongo-gateway/src/main/kotlin/tw.waterballsa.utopia.mongo.gateway/MongoCollection.kt index 261b4a32..2eec37b4 100644 --- a/mongo-gateway/src/main/kotlin/tw.waterballsa.utopia.mongo.gateway/MongoCollection.kt +++ b/mongo-gateway/src/main/kotlin/tw.waterballsa.utopia.mongo.gateway/MongoCollection.kt @@ -13,4 +13,6 @@ interface MongoCollection { fun removeAll(documents: Collection): Long fun find(query: Query): List + + fun removeAll() } diff --git a/utopia-gamification/pom.xml b/utopia-gamification/pom.xml index f8fc789b..07bbfdec 100644 --- a/utopia-gamification/pom.xml +++ b/utopia-gamification/pom.xml @@ -21,12 +21,11 @@ tw.waterballsa.utopia mongo-gateway-impl - ${revision} tw.waterballsa.utopia - mongo-gateway - ${revision} + utopia-test-kit + test diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/presenter/Presenter.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/presenter/Presenter.kt new file mode 100644 index 00000000..08fa10cd --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/presenter/Presenter.kt @@ -0,0 +1,7 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.application.presenter + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.events.AchievementAchievedEvent + +interface Presenter { + fun present(event: List) +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/AchievementRepository.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/AchievementRepository.kt new file mode 100644 index 00000000..af9bf844 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/AchievementRepository.kt @@ -0,0 +1,9 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.application.repository + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type + +interface AchievementRepository { + + fun findByType(type: Type): List +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/ProgressionRepository.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/ProgressionRepository.kt new file mode 100644 index 00000000..f8c7152e --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/repository/ProgressionRepository.kt @@ -0,0 +1,10 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.application.repository + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.* + +interface ProgressionRepository { + + fun findByPlayerIdAndAchievementType(playerId: String, type: Type): Map + + fun save(progression: Progression) +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/usecase/ProgressAchievementUseCase.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/usecase/ProgressAchievementUseCase.kt new file mode 100644 index 00000000..c9312f97 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/application/usecase/ProgressAchievementUseCase.kt @@ -0,0 +1,89 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.application.usecase + +import org.springframework.stereotype.Component +import tw.waterballsa.utopia.utopiagamification.achievement.application.presenter.Presenter +import tw.waterballsa.utopia.utopiagamification.achievement.application.repository.AchievementRepository +import tw.waterballsa.utopia.utopiagamification.achievement.application.repository.ProgressionRepository +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.* +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type.TEXT_MESSAGE +import tw.waterballsa.utopia.utopiagamification.achievement.domain.actions.Action +import tw.waterballsa.utopia.utopiagamification.achievement.domain.actions.SendMessageAction +import tw.waterballsa.utopia.utopiagamification.achievement.domain.events.AchievementAchievedEvent +import tw.waterballsa.utopia.utopiagamification.quest.domain.Player +import tw.waterballsa.utopia.utopiagamification.repositories.PlayerRepository +import tw.waterballsa.utopia.utopiagamification.repositories.exceptions.NotFoundException.Companion.notFound + +@Component +class ProgressAchievementUseCase( + private val progressionRepository: ProgressionRepository, + private val achievementRepository: AchievementRepository, + private val playerRepository: PlayerRepository +) { + + /** + * Usecase flow: + * 1. (DB) find progressions by playerId and achievement Type + * 2. create an action + * 3. progress the action, return an event + * 3-1. achievement progress action, return the result of the action (Boolean) + * 3-2. if meet achievement condition, refresh the progression (count++) + * 3-3. achievement achieve progression, return an event + * 3-3-1. if the progression count achieve the Achievement Rule, return an event + * 3-3-2. reward.reward(player) + * 4. (DB) persist the progression and player + * 5. present the events + */ + fun execute(request: Request, presenter: Presenter) { + with(request) { + // 查 + val player = playerRepository.findPlayerById(playerId) ?: throw notFound(Player::class).id(playerId).build() + val progressions = progressionRepository.findByPlayerIdAndAchievementType(playerId, type) + val achievements = achievementRepository.findByType(type) + + val action = request.toAction(player) + + // 改 + val events = achievements.mapNotNull { achievement -> + player.progress(action, progressions, achievement) + } + + // 存 + playerRepository.savePlayer(player) + + // 推 + presenter.present(events) + } + } + + data class Request( + val playerId: String, + val type: Type, + val message: String + ) { + fun toAction(player: Player): Action { + if (type == TEXT_MESSAGE) { + return SendMessageAction(player, message) + } else { + throw IllegalArgumentException("This achievement type '$type' is undefined.") + } + } + } + + private fun Player.progress( + action: Action, + progressions: Map, + achievement: Achievement, + ): AchievementAchievedEvent? { + + val progression = progressions.findProgressionByAchievement(achievement) + val refreshedProgression = achievement.progressAction(action, progression) + + progressionRepository.save(refreshedProgression) + + return achievement.achieve(this, refreshedProgression) + } + + private fun Map.findProgressionByAchievement(achievement: Achievement): Progression? = + this[achievement.name] +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/Achievement.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/Achievement.kt new file mode 100644 index 00000000..68fc6427 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/Achievement.kt @@ -0,0 +1,87 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.actions.Action +import tw.waterballsa.utopia.utopiagamification.achievement.domain.events.AchievementAchievedEvent +import tw.waterballsa.utopia.utopiagamification.quest.domain.Player +import tw.waterballsa.utopia.utopiagamification.quest.domain.Reward +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType +import java.util.UUID.randomUUID + +abstract class Achievement( + val name: Name, + val type: Type, + private val condition: Condition, + private val rule: Rule, + private val reward: Reward, +) { + + fun progressAction(action: Action, progression: Progression?): Progression { + val newProgression = Progression(randomUUID().toString(), action.player.id, name, type) + + return progress(action, progression ?: newProgression) + } + + /** + * progress flow + * 1. The player do an action + * 2. check this action is meet the condition of achievement, and return the progression + * 2-1. if meet the condition, return the progression + * 2-2. if not, return null + */ + private fun progress(action: Action, progression: Progression): Progression = + if (condition.meet(action)) progression.refresh() else progression + + /** + * achieve flow + * 1. Get the progression from progress flow + * 2. check this progression is achieved the achievement + * and player has not achieved this achievement + * 2-1. if achieved, reward the player and return the achievement-progressed-event + * 2-2. if not, return null + */ + fun achieve(player: Player, progression: Progression): AchievementAchievedEvent? { + if (rule.isAchieved(player, progression)) { + reward.reward(player) + return toAchieveEvent() + } + return null + } + + protected fun toAchieveEvent(): AchievementAchievedEvent = AchievementAchievedEvent(reward) + + enum class Name { + LONG_ARTICLE, + TOPIC_MASTER, + } + + enum class Type { + TEXT_MESSAGE + } + + class Progression( + val id: String, + val playerId: String, + val name: Name, + val type: Type, + var count: Int = 0, + ) { + fun refresh(): Progression { + count++ + return this + } + + fun isAchieved(achievementCount: Int): Boolean = count == achievementCount + } + + interface Condition { + fun meet(action: Action): Boolean + } + + class Rule( + private val role: RoleType, + private val achievedCount: Int + ) { + fun isAchieved(player: Player, progression: Progression): Boolean = + !player.hasRole(role.name) && progression.isAchieved(achievedCount) + } +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/LongArticleAchievement.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/LongArticleAchievement.kt new file mode 100644 index 00000000..d83a9285 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/LongArticleAchievement.kt @@ -0,0 +1,30 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Name.LONG_ARTICLE +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type.TEXT_MESSAGE +import tw.waterballsa.utopia.utopiagamification.achievement.domain.actions.Action +import tw.waterballsa.utopia.utopiagamification.achievement.domain.actions.SendMessageAction +import tw.waterballsa.utopia.utopiagamification.quest.domain.Reward + +class LongArticleAchievement( + condition: Condition, + rule: Rule, + reward: Reward +) : Achievement( + LONG_ARTICLE, + TEXT_MESSAGE, + condition, + rule, + reward +) { + + /** + * LongArticle.Condition:判斷此次發送的訊息字數是否達到 800 字 + */ + class Condition( + private val wordLength: Int + ) : Achievement.Condition { + override fun meet(action: Action): Boolean = + action is SendMessageAction && action.contentWordRequirement(wordLength) + } +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/TopicMasterAchievement.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/TopicMasterAchievement.kt new file mode 100644 index 00000000..4067a93c --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/achievements/TopicMasterAchievement.kt @@ -0,0 +1,24 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Name.TOPIC_MASTER +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type.TEXT_MESSAGE +import tw.waterballsa.utopia.utopiagamification.achievement.domain.actions.Action +import tw.waterballsa.utopia.utopiagamification.quest.domain.Reward + + +class TopicMasterAchievement( + condition: Condition, + rule: Rule, + reward: Reward +) : Achievement( + TOPIC_MASTER, + TEXT_MESSAGE, + condition, + rule, + reward +) { + + class Condition : Achievement.Condition { + override fun meet(action: Action): Boolean = true + } +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/Action.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/Action.kt new file mode 100644 index 00000000..5085ce18 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/Action.kt @@ -0,0 +1,22 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.domain.actions + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Progression +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type +import tw.waterballsa.utopia.utopiagamification.achievement.domain.events.AchievementAchievedEvent +import tw.waterballsa.utopia.utopiagamification.quest.domain.Player + +open class Action( + val type: Type, + val player: Player +) { + /** + * 1. progress the action, return a progression + * 2. received progression achieve progress, return an AchievementEvent + */ + fun progress(achievement: Achievement, progression: Progression): Progression = + achievement.progressAction(this, progression) + + fun achieve(achievement: Achievement, progression: Progression): AchievementAchievedEvent? = + achievement.achieve(player, progression) +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/SendMessageAction.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/SendMessageAction.kt new file mode 100644 index 00000000..7ecb511d --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/actions/SendMessageAction.kt @@ -0,0 +1,12 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.domain.actions + +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type.TEXT_MESSAGE +import tw.waterballsa.utopia.utopiagamification.quest.domain.Player + +open class SendMessageAction( + player: Player, + private val words: String, +) : Action(TEXT_MESSAGE, player) { + + fun contentWordRequirement(wordLength: Int): Boolean = words.length >= wordLength +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/events/AchievementAchievedEvent.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/events/AchievementAchievedEvent.kt new file mode 100644 index 00000000..84e784ab --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/domain/events/AchievementAchievedEvent.kt @@ -0,0 +1,7 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.domain.events + +import tw.waterballsa.utopia.utopiagamification.quest.domain.Reward + +class AchievementAchievedEvent( + val reward: Reward, +) diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/AchievementDao.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/AchievementDao.kt new file mode 100644 index 00000000..ec437df0 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/AchievementDao.kt @@ -0,0 +1,37 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.framework.dao + +import org.springframework.stereotype.Component +import tw.waterballsa.utopia.utopiagamification.achievement.application.repository.AchievementRepository +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Rule +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.LongArticleAchievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.TopicMasterAchievement +import tw.waterballsa.utopia.utopiagamification.quest.domain.Reward +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType.LONG_ARTICLE +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType.TOPIC_MASTER + +@Component +class AchievementDao : AchievementRepository { + + private val achievements = mutableListOf() + + init { + achievements.addAll( + listOf( + LongArticleAchievement( + LongArticleAchievement.Condition(800), + Rule(LONG_ARTICLE, 1), + Reward(1000u, LONG_ARTICLE) + ), + TopicMasterAchievement( + TopicMasterAchievement.Condition(), + Rule(TOPIC_MASTER, 300), + Reward(2500u, TOPIC_MASTER) + ) + ) + ) + } + + override fun findByType(type: Type): List = achievements.filter { it.type == type } +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/ProgressionDao.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/ProgressionDao.kt new file mode 100644 index 00000000..54033ad8 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/ProgressionDao.kt @@ -0,0 +1,35 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.framework.dao + +import org.springframework.stereotype.Component +import tw.waterballsa.utopia.mongo.gateway.Criteria +import tw.waterballsa.utopia.mongo.gateway.MongoCollection +import tw.waterballsa.utopia.mongo.gateway.Query +import tw.waterballsa.utopia.utopiagamification.achievement.application.repository.ProgressionRepository +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.* +import tw.waterballsa.utopia.utopiagamification.achievement.framework.dao.documents.ProgressionDocument +import java.util.UUID.randomUUID + +@Component +class ProgressionDao( + private val repository: MongoCollection +) : ProgressionRepository { + + override fun findByPlayerIdAndAchievementType( + playerId: String, + type: Type + ): Map { + return repository.find( + Query( + Criteria("playerId").`is`(playerId) + .and("achievementType").`is`(type) + ) + ).associate { it.achievementName to it.toDomain() } + } + + override fun save(progression: Progression) { + repository.save(progression.toDocument()) + } + + private fun Progression.toDocument(): ProgressionDocument = + ProgressionDocument(id, playerId, type, name, count) +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/documents/ProgressionDocument.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/documents/ProgressionDocument.kt new file mode 100644 index 00000000..b68b99f1 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/dao/documents/ProgressionDocument.kt @@ -0,0 +1,16 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.framework.dao.documents + +import tw.waterballsa.utopia.mongo.gateway.Document +import tw.waterballsa.utopia.mongo.gateway.Id +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.* + +@Document +class ProgressionDocument( + @Id val id: String, + val playerId: String, + val achievementType: Type, + val achievementName: Name, + val count: Int +) { + fun toDomain(): Progression = Progression(id, playerId, achievementName, achievementType, count) +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/AchievementListener.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/AchievementListener.kt new file mode 100644 index 00000000..5c1407f6 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/AchievementListener.kt @@ -0,0 +1,45 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.framework.listener + +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import org.springframework.stereotype.Component +import tw.waterballsa.utopia.jda.UtopiaListener +import tw.waterballsa.utopia.utopiagamification.achievement.application.usecase.ProgressAchievementUseCase +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type.TEXT_MESSAGE +import tw.waterballsa.utopia.utopiagamification.achievement.framework.listener.enums.DiscordRole +import tw.waterballsa.utopia.utopiagamification.achievement.framework.listener.presenter.ProgressAchievementPresenter + + +@Component +class AchievementListener( + private val discordRole: DiscordRole, + private val progressAchievementUseCase: ProgressAchievementUseCase +) : UtopiaListener() { + + override fun onMessageReceived(event: MessageReceivedEvent) { + with(event) { + if (author.isBot) return + + val presenter = ProgressAchievementPresenter() + val action = ProgressAchievementUseCase.Request( + author.id, + TEXT_MESSAGE, + message.contentDisplay, + ) + + progressAchievementUseCase.execute(action, presenter) + addRolesToPlayer(presenter) + if (presenter.isViewModelsNotEmpty()) { + channel.sendMessage(presenter.toMessage()).queue() + } + } + } + + private fun MessageReceivedEvent.addRolesToPlayer(presenter: ProgressAchievementPresenter) { + presenter.toRoleIds().forEach { + guild.addRoleToMember(author, guild.getRoleById(it)!!).complete() + } + } + + private fun ProgressAchievementPresenter.toRoleIds(): List = + progressAchievementViewModels.map { discordRole.getRoleId(it.roleType) } +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/enums/DiscordRole.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/enums/DiscordRole.kt new file mode 100644 index 00000000..2446d90e --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/enums/DiscordRole.kt @@ -0,0 +1,26 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.framework.listener.enums + +import org.springframework.stereotype.Component +import org.testcontainers.shaded.org.bouncycastle.asn1.x500.style.RFC4519Style.name +import tw.waterballsa.utopia.commons.config.WsaDiscordProperties +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType + +/** + * 把 DiscordRole 和 Domain RoleType 用 RoleName 作為 key 連結起來 + * 1. Map + * 2. 從 wsa properties 讀取 jda role id + */ +@Component +class DiscordRole( + private val properties: WsaDiscordProperties +) { + private val roleTypeToId = mutableMapOf() + + init { + roleTypeToId[RoleType.LONG_ARTICLE] = properties.wsaLongArticleRoleId + roleTypeToId[RoleType.TOPIC_MASTER] = properties.wsaTopicMasterRoleId + } + + fun getRoleId(roleType: RoleType): String = + roleTypeToId[roleType] ?: throw IllegalArgumentException("The role type $name is not valid, as the role type has not been defined.") +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/presenter/ProgressAchievementPresenter.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/presenter/ProgressAchievementPresenter.kt new file mode 100644 index 00000000..05443656 --- /dev/null +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/achievement/framework/listener/presenter/ProgressAchievementPresenter.kt @@ -0,0 +1,29 @@ +package tw.waterballsa.utopia.utopiagamification.achievement.framework.listener.presenter + +import tw.waterballsa.utopia.utopiagamification.achievement.application.presenter.Presenter +import tw.waterballsa.utopia.utopiagamification.achievement.domain.events.AchievementAchievedEvent +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType + +class ProgressAchievementPresenter : Presenter { + val progressAchievementViewModels = mutableListOf() + + override fun present(event: List) { + for (achievedEvent in event) { + progressAchievementViewModels.add(achievedEvent.toViewModel()) + } + } + + fun toMessage(): String = progressAchievementViewModels.joinToString("\n") { + "恭喜達成 **_${it.roleType.description}_** 成就,取得 **_${it.exp}_** 點經驗值!" + } + + fun isViewModelsNotEmpty(): Boolean = progressAchievementViewModels.isNotEmpty() + + private fun AchievementAchievedEvent.toViewModel(): ProgressAchievementViewModel = + ProgressAchievementViewModel(reward.exp.toLong(), reward.role!!) + + data class ProgressAchievementViewModel( + val exp: Long, + val roleType: RoleType, + ) +} diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt index c0863d20..aeeddb8d 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Player.kt @@ -13,6 +13,7 @@ class Player( val joinDate: OffsetDateTime = now(), var latestActivateDate: OffsetDateTime = now(), var levelUpgradeDate: OffsetDateTime = now(), + // TODO achievement-system 這邊應該要改成 Role 陣列 val jdaRoles: MutableList = mutableListOf(), ) { @@ -29,17 +30,13 @@ class Player( activate() } - private fun calculateLevel() { - var explimit = getLevelExpLimit(level) - while (exp >= explimit) { - exp -= explimit - level++ - explimit = getLevelExpLimit(level) - levelUpgradeDate = now() - } + fun hasRole(role: String): Boolean = jdaRoles.contains(role) + + fun addRole(role: String){ + jdaRoles.add(role) } - private fun calculateLevel1() { + private fun calculateLevel() { val newLevel = calculateLevel(exp) if (newLevel > level) { level = newLevel diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Quest.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Quest.kt index d9e530ec..c1872623 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Quest.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/domain/Quest.kt @@ -17,23 +17,33 @@ class Quest( class Reward( val exp: ULong, - //TODO:未來實作商店功能使用金幣兌換道具(職涯攻略、範例程式碼) + // TODO:未來實作商店功能使用金幣兌換道具(職涯攻略、範例程式碼) val coin: ULong, val bonus: Float, val role: RoleType? ) { constructor(exp: ULong, coin: ULong, bonus: Float) : this(exp, coin, bonus, null) + constructor(exp: ULong, role: RoleType) : this(exp, 0uL, 0f, role) + + fun reward(player: Player) { + player.gainExp(exp) + player.addRole(role!!.name) + } } +// TODO add description enum class RoleType( + val description: String, val level: Int ) { - EVERYONE(0), - WSA_MEMBER(1), - GENTLEMAN(2), - SENIOR_GENTLEMAN(3), - MENTOR(3); + LONG_ARTICLE("長文成就", 0), + TOPIC_MASTER("話題高手", 0), + EVERYONE("學院公民", 0), + WSA_MEMBER("水球成員", 1), + GENTLEMAN("學院紳士", 2), + SENIOR_GENTLEMAN("資深紳士", 3), + MENTOR("學院導師", 3); fun isHigherThanRoleLevel(level: Int): Boolean = level >= this.level } diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt index e5e95181..824a6ba1 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/quest/extensions/LevelSheet.kt @@ -1,6 +1,6 @@ package tw.waterballsa.utopia.utopiagamification.quest.extensions -import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.LevelRange.Companion.LEVEL_ONE +import tw.waterballsa.utopia.utopiagamification.quest.extensions.LevelSheet.Range.Companion.LEVEL_ONE import kotlin.ULong.Companion.MIN_VALUE class LevelSheet private constructor() { @@ -9,24 +9,31 @@ class LevelSheet private constructor() { const val EXP_PER_MINUTES = 10u private const val MAX_LEVEL = 100 private val COEFFICIENTS = arrayOf(1u, 2u, 4u, 8u, 12u, 16u, 32u, 52u, 64u, 84u) - private val LEVEL_RANGES = generateSequence(LEVEL_ONE) { it.next() }.take(MAX_LEVEL) + private val LEVEL_TO_RANGE = generateSequence(LEVEL_ONE) { it.next() }.take(MAX_LEVEL).associateBy { it.level } - fun calculateLevel(exp: ULong) = (LEVEL_RANGES.find { it.isExpGreaterThan(exp) } ?: LEVEL_ONE).level.toUInt() + fun calculateLevel(exp: ULong) = (LEVEL_TO_RANGE.values.find { it.isExpGreaterThan(exp) } ?: LEVEL_ONE).level.toUInt() + + fun getLevelRange(level: Int): Range = when { + level <= 0 -> LEVEL_ONE + LEVEL_TO_RANGE.contains(level) -> LEVEL_TO_RANGE[level]!! + level > MAX_LEVEL -> LEVEL_TO_RANGE.values.last() + else -> throw IllegalArgumentException("The level ($level) is incorrect.") + } } - private class LevelRange private constructor(val level: Int = 1, previousLevelRange: LevelRange? = null) { + class Range private constructor(val level: Int = 1, previousLevelRange: Range? = null) { // 升級時間 - private val upgradeTime: ULong + val upgradeTime: ULong // 累積經驗值 - private val accExp: ULong + val accExp: ULong // 當前經驗值上限 - private val expLimit: ULong + val expLimit: ULong companion object { - val LEVEL_ONE = LevelRange() + val LEVEL_ONE = Range() } init { @@ -37,7 +44,7 @@ class LevelSheet private constructor() { expLimit = accExp.minus(previousLevelRange?.accExp ?: MIN_VALUE) } - fun next() = LevelRange(level.plus(1), this) + fun next() = Range(level.plus(1), this) fun isExpGreaterThan(exp: ULong) = accExp > exp diff --git a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt index 094d82a2..1f692b8c 100644 --- a/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt +++ b/utopia-gamification/src/main/kotlin/tw/waterballsa/utopia/utopiagamification/repositories/mongodb/repositoryimpl/MongodbPlayerRepository.kt @@ -24,7 +24,8 @@ class MongodbPlayerRepository( level.toUInt(), joinDate, latestActivateDate, - levelUpgradeDate + levelUpgradeDate, + jdaRoles ) private fun Player.toDocument(): PlayerDocument = PlayerDocument( @@ -34,7 +35,8 @@ class MongodbPlayerRepository( level.toInt(), joinDate, latestActivateDate, - levelUpgradeDate + levelUpgradeDate, + jdaRoles ) } @@ -47,4 +49,5 @@ data class PlayerDocument( val joinDate: OffsetDateTime, val latestActivateDate: OffsetDateTime, val levelUpgradeDate: OffsetDateTime, + val jdaRoles: MutableList ) diff --git a/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/it/AchievementIntegrationTest.kt b/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/it/AchievementIntegrationTest.kt new file mode 100644 index 00000000..4ce530cc --- /dev/null +++ b/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/it/AchievementIntegrationTest.kt @@ -0,0 +1,205 @@ +package tw.waterballsa.utopia.utopiagmification.achievement.it + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import tw.waterballsa.utopia.utopiagamification.achievement.application.repository.ProgressionRepository +import tw.waterballsa.utopia.utopiagamification.achievement.application.usecase.ProgressAchievementUseCase +import tw.waterballsa.utopia.utopiagamification.achievement.application.usecase.ProgressAchievementUseCase.Request +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Progression +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type.TEXT_MESSAGE +import tw.waterballsa.utopia.utopiagamification.achievement.framework.listener.presenter.ProgressAchievementPresenter +import tw.waterballsa.utopia.utopiagamification.quest.domain.Player +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType.LONG_ARTICLE +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType.TOPIC_MASTER +import tw.waterballsa.utopia.utopiagamification.repositories.PlayerRepository +import tw.waterballsa.utopia.utopiatestkit.annotations.UtopiaTest +import java.util.UUID.randomUUID + +@UtopiaTest +class AchievementIntegrationTest @Autowired constructor( + private val progressAchievementUsecase: ProgressAchievementUseCase, + private val playerRepository: PlayerRepository, + private val progressionRepository: ProgressionRepository +) { + private lateinit var playerA: Player + + private lateinit var sendTwoHundredNinetyNineMessageProgression: Progression + + @BeforeEach + fun setup() { + playerA = Player( + id = randomUUID().toString(), + name = "A", + exp = 1200uL, + jdaRoles = mutableListOf() + ) + sendTwoHundredNinetyNineMessageProgression = Progression( + randomUUID().toString(), + playerId = playerA.id, + name = Achievement.Name.TOPIC_MASTER, + type = TEXT_MESSAGE, + count = 299 + ) + playerRepository.savePlayer(playerA) + } + + @Test + @DisplayName( + """ + 訊息太短,無法觸發長文 + Given: + - 玩家 A 沒有「長文成就」身份組 + - 玩家 A 的 EXP = 1200 + When: + - 玩家 A 在文字頻道,或是某個論壇輸入訊息 “Test123456” + Then: + - 玩家 A 仍然沒有「長文成就」身份組 + - 玩家 A 的 EXP = 1200 + """ + ) + fun `player doesn't achieve the article-achievement`() { + + val request = Request(playerA.id, TEXT_MESSAGE, "Test123456") + val presenter = ProgressAchievementPresenter() + + // when + progressAchievementUsecase.execute(request, presenter) + + // then + val player = playerRepository.findPlayerById(playerA.id) + assertThat(player).isNotNull + assertThat(player?.exp).isEqualTo(1200uL) + assertThat(player?.hasRole(LONG_ARTICLE.description)).isFalse() + } + + @Test + @DisplayName( + """ + 發布超過 800 的字的長文 + 玩家 A 發表了一篇長文 + Given: + - 玩家 A 沒有「長文成就」身份組 + - 玩家 A 在文字頻道,或是某個論壇發表了一篇字數大於 800 字的貼文 + - 玩家 A EXP = 1200 + When: + - 玩家 A 發佈文章 + Then: + - 玩家 A 獲得「長文成就」身份組,EXP = 2200 + """ + ) + fun `player achieve the article-achievement`() { + val article = "1".repeat(1000) + + val request = Request(playerA.id, TEXT_MESSAGE, article) + val presenter = ProgressAchievementPresenter() + + //when + progressAchievementUsecase.execute(request, presenter) + + //then + val player = playerRepository.findPlayerById(playerA.id) + + assertThat(player).isNotNull() + assertThat(player?.exp).isEqualTo(2200uL) + assertThat(player?.hasRole(LONG_ARTICLE.name)).isTrue() + } + + @Test + @DisplayName( + """ + 留言數過少,無法得到話題高手 + Given: + - 玩家 A 沒有「話題高手」身份組 + - 玩家 A EXP = 1200 + When: + - 玩家 A 發佈 1 則留言 + Then: + - 玩家 A 沒有「話題高手」身份組 + - 玩家 A EXP = 1200 + """ + ) + fun `player doesn't achieve the topic-master-achievement`() { + + val request = Request(playerA.id, TEXT_MESSAGE, "一二三四五") + val presenter = ProgressAchievementPresenter() + + // when + + progressAchievementUsecase.execute(request, presenter) + + // then + + val player = playerRepository.findPlayerById(playerA.id) + assertThat(player).isNotNull + assertThat(player?.hasRole(TOPIC_MASTER.description)).isFalse() + assertThat(player?.exp).isEqualTo(1200uL) + } + + @Test + @DisplayName( + """ + 超過三百則,獲得話題高手 + Given: + - 玩家 A 已經發言過 299 則訊息 + - 玩家 A 沒有「話題高手」身份組 + - 玩家 A EXP = 1200 + When:當玩家 A 再送出一則訊息「ABC」 + Then: + - 玩家 A 獲得「話題高手」成就的訊息 + - 玩家 A EXP = 3700 + """ + ) + fun `player achieve the topic-master-achievement`() { + + progressionRepository.save(sendTwoHundredNinetyNineMessageProgression) + + val request = Request(playerA.id, TEXT_MESSAGE, "一二三四五") + val presenter = ProgressAchievementPresenter() + + // when + progressAchievementUsecase.execute(request, presenter) + + val player = playerRepository.findPlayerById(playerA.id) + + // then + assertThat(player).isNotNull() + assertThat(player?.exp).isEqualTo(3700uL) + assertThat(player?.hasRole(TOPIC_MASTER.name)).isTrue() + } + + @Test + @DisplayName( + """ + 超過三百則,獲得話題高手,並且發布超過 800 的字的長文 + Given: + 玩家 A 已經發言過 299 則訊息 + 玩家 A 沒有「話題高手」身份組 + 玩家 A 沒有「長文成就」身份組 + 玩家 A EXP = 1200 + When:當玩家 A 再送出一篇字數大於 800 字的貼文 + Then: + 玩家 A 獲得「話題高手」成就的訊息 + 玩家 A 獲得「長文成就」身份組 + 玩家 A EXP = 4700 + """ + ) + fun `player achieve the topic-master-achievement and article-achievement`() { + + progressionRepository.save(sendTwoHundredNinetyNineMessageProgression) + val request = Request(playerA.id, TEXT_MESSAGE, "1".repeat(801)) + val presenter = ProgressAchievementPresenter() + // when + progressAchievementUsecase.execute(request, presenter) + + // then + val player = playerRepository.findPlayerById(playerA.id) + assertThat(player).isNotNull() + assertThat(player?.exp).isEqualTo(4700uL) + assertThat(player?.hasRole(TOPIC_MASTER.name)).isTrue() + assertThat(player?.hasRole(LONG_ARTICLE.name)).isTrue() + } +} diff --git a/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/ut/AchievementUnitTest.kt b/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/ut/AchievementUnitTest.kt new file mode 100644 index 00000000..8ddb7e33 --- /dev/null +++ b/utopia-gamification/src/test/kotlin/tw/waterballsa/utopia/utopiagmification/achievement/ut/AchievementUnitTest.kt @@ -0,0 +1,171 @@ +package tw.waterballsa.utopia.utopiagmification.achievement.ut + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Name +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Rule +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type.TEXT_MESSAGE +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.LongArticleAchievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.TopicMasterAchievement +import tw.waterballsa.utopia.utopiagamification.achievement.domain.actions.SendMessageAction +import tw.waterballsa.utopia.utopiagamification.quest.domain.Player +import tw.waterballsa.utopia.utopiagamification.quest.domain.Reward +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType.LONG_ARTICLE +import tw.waterballsa.utopia.utopiagamification.quest.domain.RoleType.TOPIC_MASTER +import java.util.UUID.randomUUID + +class AchievementUnitTest { + + private lateinit var playerA: Player + + private lateinit var progression: Achievement.Progression + + private val longArticleAchievement = LongArticleAchievement( + LongArticleAchievement.Condition(800), + Rule(LONG_ARTICLE, 1), + Reward(1000u, LONG_ARTICLE) + ) + + private val topicMasterAchievement = TopicMasterAchievement( + TopicMasterAchievement.Condition(), + Rule(TOPIC_MASTER, 300), + Reward(2500u, TOPIC_MASTER) + ) + + @BeforeEach + fun setup() { + playerA = Player( + id = randomUUID().toString(), + name = "A", + exp = 1200uL, + jdaRoles = mutableListOf() + ) + + progression = Achievement.Progression( + randomUUID().toString(), + playerId = playerA.id, + name = Name.TOPIC_MASTER, + type = TEXT_MESSAGE, + count = 0 + ) + } + + @Test + @DisplayName( + """ + 訊息太短,無法觸發長文 + Given: + - 玩家 A 沒有「長文成就」身份組 + - 玩家 A 的 EXP = 1200 + When: + - 玩家 A 在文字頻道,或是某個論壇輸入訊息 “Test123456” + Then: + - 玩家 A 仍然沒有「長文成就」身份組 + - 玩家 A 的 EXP = 1200 + """ + ) + fun `player doesn't achieve the article-achievement`() { + + // when + val sendMessageAction = playerA.sendMessage("Test123456") + val progression = sendMessageAction.progress(longArticleAchievement, progression) + sendMessageAction.achieve(longArticleAchievement, progression) + + // then + assertEquals(playerA.exp, 1200uL) + assertFalse(playerA.hasRole(LONG_ARTICLE.description)) + } + + + @Test + @DisplayName( + """ + 發布超過 800 的字的長文 + 玩家 A 發表了一篇長文 + Given: + - 玩家 A 沒有「長文成就」身份組 + - 玩家 A 在文字頻道,或是某個論壇發表了一篇字數大於 800 字的貼文 + - 玩家 A EXP = 1200 + When: + - 玩家 A 發佈文章 + Then: + - 玩家 A 獲得「長文成就」身份組,EXP = 2200 + """ + ) + fun `player achieve the article-achievement`() { + val article = "1".repeat(1000) + + //when + val sendMessageAction = playerA.sendMessage(article) + //then + val progression = sendMessageAction.progress(longArticleAchievement, progression) + sendMessageAction.achieve(longArticleAchievement, progression) + + assertEquals(playerA.exp, 2200uL) + assertTrue(playerA.hasRole(LONG_ARTICLE.name)) + } + + @Test + @DisplayName( + """ + 留言數過少,無法得到話題高手 + Given: + - 玩家 A 沒有「話題高手」身份組 + - 玩家 A EXP = 1200 + When: + - 玩家 A 發佈 1 則留言 + Then: + - 玩家 A 沒有「話題高手」身份組 + - 玩家 A EXP = 1200 + """ + ) + fun `player doesn't achieve the topic-master-achievement`() { + // when + val sendMessageAction = playerA.sendMessage("一二三四五") + val progression = sendMessageAction.progress(topicMasterAchievement, progression) + sendMessageAction.achieve(topicMasterAchievement, progression) + + // then + assertFalse(playerA.hasRole(TOPIC_MASTER.description)) + assertEquals(playerA.exp, 1200uL) + } + + @Test + @DisplayName( + """ + 超過三百則,獲得話題高手 + Given: + - 玩家 A 已經發言過 299 則訊息 + - 玩家 A 沒有「話題高手」身份組 + - 玩家 A EXP = 1200 + When:當玩家 A 再送出一則訊息「ABC」 + Then: + - 玩家 A 獲得「話題高手」成就的訊息 + - 玩家 A EXP = 3700 + """ + ) + fun `player achieve the topic-master-achievement`() { + val sendTwoHundredNinetyNineMessageProgression = Achievement.Progression( + randomUUID().toString(), + playerId = playerA.id, + name = Name.TOPIC_MASTER, + type = TEXT_MESSAGE, + count = 299 + ) + // when + val sendMessageAction = playerA.sendMessage("一二三四五") + val progression = + sendMessageAction.progress(topicMasterAchievement, sendTwoHundredNinetyNineMessageProgression) + sendMessageAction.achieve(topicMasterAchievement, progression) + + // then + assertTrue(playerA.hasRole(TOPIC_MASTER.name)) + assertEquals(playerA.exp, 3700uL) + } + + private fun Player.sendMessage(words: String): SendMessageAction = + SendMessageAction(this, words) +} diff --git a/utopia-test-kit/src/main/koltin/tw/waterballsa/utopia/utopiatestkit/configs/MongoDbTestContainerConfig.kt b/utopia-test-kit/src/main/koltin/tw/waterballsa/utopia/utopiatestkit/configs/MongoDbTestContainerConfig.kt index 45084b09..e57f954c 100644 --- a/utopia-test-kit/src/main/koltin/tw/waterballsa/utopia/utopiatestkit/configs/MongoDbTestContainerConfig.kt +++ b/utopia-test-kit/src/main/koltin/tw/waterballsa/utopia/utopiatestkit/configs/MongoDbTestContainerConfig.kt @@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Configuration import org.springframework.data.mongodb.core.MongoTemplate import org.testcontainers.containers.MongoDBContainer -import java.lang.System.setProperty +import java.lang.System.* @Configuration open class MongoDbTestContainerConfig : BeforeEachCallback, AfterAllCallback { @@ -43,4 +43,4 @@ open class MongoDbTestContainerConfig : BeforeEachCallback, AfterAllCallback { } } -} \ No newline at end of file +}