Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add achievement system #209

Merged
merged 6 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
}
}
2 changes: 2 additions & 0 deletions main/src/main/resources/wsa.beta.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ flag-post-guide-id=1072845227418714193
water-ball-journal-post-id=1072869148826292234
water-ball-lose-weight-post-id=1091190313575526400
wsa-guideline-channel-id=1042774419371720715
wsa-long-article-role-id=1163842900933742592
wsa-topic-master-role-id=1163842370018754560
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
5 changes: 2 additions & 3 deletions utopia-gamification/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@
<dependency>
<groupId>tw.waterballsa.utopia</groupId>
<artifactId>mongo-gateway-impl</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>tw.waterballsa.utopia</groupId>
<artifactId>mongo-gateway</artifactId>
<version>${revision}</version>
<artifactId>utopia-test-kit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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(events: List<AchievementAchievedEvent>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package tw.waterballsa.utopia.utopiagamification.achievement.application.repository

import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement

interface AchievementRepository {

fun findByType(type: Achievement.Type): List<Achievement>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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.Progression

interface ProgressionRepository {

fun findByPlayerIdAndAchievementType(playerId: String, type: Achievement.Type): List<Progression>

fun save(progression: Progression): Progression
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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.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 = findPlayer()
val achievements = achievementRepository.findByType(type)
val achievementNameToProgression = achievementNameToProgression()

// 改
val action = toAction(player)
val events = achievements.mapNotNull { achievement ->
val progression = achievementNameToProgression[achievement.name]
action.progress(achievement, progression)
}

// 存
playerRepository.savePlayer(player)

// 推
presenter.present(events)
}
}

private fun Request.findPlayer(): Player =
playerRepository.findPlayerById(playerId) ?: throw notFound(Player::class).id(playerId).build()

private fun Request.achievementNameToProgression(): Map<Achievement.Name, Achievement.Progression> =
progressionRepository.findByPlayerIdAndAchievementType(playerId, type)
.associateBy { it.name }

private fun Request.toAction(player: Player): Action {
return if (type == TEXT_MESSAGE) {
SendMessageAction(player, message)
} else {
throw IllegalArgumentException("This achievement type '$type' is undefined.")
}
}

private fun Action.progress(achievement: Achievement, progression: Achievement.Progression?): AchievementAchievedEvent? {
val refreshedProgression = progressionRepository.save(achievement.progressAction(this, progression))
return achievement.achieve(player, refreshedProgression)
}

data class Request(
val playerId: String,
val type: Achievement.Type,
val message: String
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 =
progress(action, progression ?: Progression(randomUUID().toString(), action.player.id, name, type))

/**
* 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? {
return if (rule.isAchieved(player, progression)) {
reward.reward(player)
toAchieveEvent()
} else 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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tw.waterballsa.utopia.utopiagamification.achievement.domain.actions

import tw.waterballsa.utopia.utopiagamification.achievement.domain.achievements.Achievement.Type
import tw.waterballsa.utopia.utopiagamification.quest.domain.Player

open class Action(
val type: Type,
val player: Player
)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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.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<Achievement>()

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: Achievement.Type): List<Achievement> = achievements.filter { it.type == type }
}
Loading