Skip to content

Commit

Permalink
✨ achievement-system
Browse files Browse the repository at this point in the history
  • Loading branch information
m1a2st committed Oct 23, 2023
1 parent e220689 commit f7e7063
Show file tree
Hide file tree
Showing 27 changed files with 919 additions and 32 deletions.
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")
}
}
}
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 Expand Up @@ -48,6 +48,10 @@ class MongoCollectionAdapter<TDocument, ID>(
).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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ interface MongoCollection<TDocument, ID> {
fun removeAll(documents: Collection<TDocument>): Long

fun find(query: Query): List<TDocument>

fun removeAll()
}
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(event: List<AchievementAchievedEvent>)
}
Original file line number Diff line number Diff line change
@@ -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<Achievement>
}
Original file line number Diff line number Diff line change
@@ -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<Name, Progression>

fun save(progression: Progression)
}
Original file line number Diff line number Diff line change
@@ -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<Name, Progression>,
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<Name, Progression>.findProgressionByAchievement(achievement: Achievement): Progression? =
this[achievement.name]
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
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,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)
}
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,
)
Loading

0 comments on commit f7e7063

Please sign in to comment.