Skip to content

Commit

Permalink
#9 simplified server side projection to only needed data
Browse files Browse the repository at this point in the history
  • Loading branch information
stoerti committed Jul 8, 2024
1 parent b922f2f commit 03ccc18
Show file tree
Hide file tree
Showing 12 changed files with 58 additions and 556 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import org.quizmania.game.api.*
import org.quizmania.game.common.GameConfig
import org.quizmania.game.common.GameQuestionId
import org.quizmania.game.common.GameUserId
import org.quizmania.game.common.QuestionType
import org.quizmania.rest.application.domain.*
import org.quizmania.rest.application.domain.GameEntity
import org.quizmania.rest.application.domain.GameStatus
import org.quizmania.rest.application.domain.GameUserEntity
import org.quizmania.rest.port.`in`.FindGamePort
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.*
import java.time.Instant
import java.util.*

@RestController
Expand Down Expand Up @@ -206,34 +206,11 @@ data class GameDto(
val moderator: String?,
val status: GameStatus,
val users: List<GameUserDto>,
val questions: List<GameQuestionDto>
)

data class GameUserDto(
val id: UUID,
val name: String,
val points: Int
)

data class GameQuestionDto(
val id: UUID,
val type: QuestionType,
val questionNumber: Int,
val phrase: String,
val imagePath: String?,
val status: QuestionStatus,
val correctAnswer: String?,
val answerOptions: List<String>,
val questionTimeout: Long,
val questionAsked: Instant,
val userAnswers: List<UserAnswerDto>
)

data class UserAnswerDto(
val id: UUID,
val gameUserId: UUID,
val answer: String,
val points: Int?
)

fun GameEntity.toDto(): GameDto {
Expand All @@ -246,35 +223,9 @@ fun GameEntity.toDto(): GameDto {
moderator = moderator,
status = status,
users = users.map { it.toDto() },
questions = questions.map { it.toDto(questionTimeout) },
)
}

fun GameUserEntity.toDto(): GameUserDto {
return GameUserDto(gameUserId, username, points)
}

fun GameQuestionEntity.toDto(questionTimeout: Long): GameQuestionDto {
return GameQuestionDto(
id = gameQuestionId,
type = type,
questionNumber = questionNumber,
phrase = questionPhrase,
imagePath = questionImagePath,
status = status,
correctAnswer = correctAnswer,
answerOptions = answerOptions,
questionTimeout = questionTimeout,
questionAsked = questionAsked,
userAnswers = userAnswers.map { it.toDto(status == QuestionStatus.OPEN) }
)
}

fun UserAnswerEntity.toDto(mask: Boolean): UserAnswerDto {
return UserAnswerDto(
id = userAnswerId,
gameUserId = gameUserId,
answer = if (mask) "******" else answer,
points = points
)
return GameUserDto(gameUserId, username)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ class GameEntity(
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
@JoinColumn(name = "game_id")
var users: MutableList<GameUserEntity> = mutableListOf(),

@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
@JoinColumn(name = "game_id")
var questions: MutableList<GameQuestionEntity> = mutableListOf()
) {

constructor(event: GameCreatedEvent) : this(
Expand All @@ -39,70 +35,8 @@ class GameEntity(
status = GameStatus.CREATED
)

fun on(event: QuestionAskedEvent, eventTimestamp: Instant) {
this.questions.add(
GameQuestionEntity(
gameQuestionId = event.gameQuestionId,
type = event.question.type,
questionNumber = event.gameQuestionNumber,
questionPhrase = event.question.phrase,
questionImagePath = event.question.imagePath,
questionAsked = eventTimestamp,
status = QuestionStatus.OPEN,
correctAnswer = event.question.correctAnswer,
answerOptions = if (event.question is ChoiceQuestion) event.question.answerOptions.toMutableList() else mutableListOf()
)
)
}

fun on(event: QuestionAnsweredEvent) {
questions.find { it.gameQuestionId == event.gameQuestionId }?.userAnswers?.add(
UserAnswerEntity(
userAnswerId = event.userAnswerId,
gameUserId = event.gameUserId,
answer = event.answer
)
)
}

fun on(event: QuestionAnswerOverriddenEvent) {
questions.find { it.gameQuestionId == event.gameQuestionId }?.let { question ->
question.userAnswers.first { it.userAnswerId == event.userAnswerId }.let {
it.answer = event.answer
}
}
}

fun on(event: QuestionClosedEvent) {
questions.find { it.gameQuestionId == event.gameQuestionId }?.let { question ->
question.status = QuestionStatus.CLOSED
}
}

fun on(event: QuestionRatedEvent) {
questions.find { it.gameQuestionId == event.gameQuestionId }?.let { question ->
question.status = QuestionStatus.RATED
event.points.forEach { entry ->
question.userAnswers.find { user -> user.gameUserId == entry.key }?.let {
it.points = entry.value
}
users.find { user -> user.gameUserId == entry.key }?.let {
it.points += entry.value
}
}
}

// wrong answers scored 0 points
questions.find { it.gameQuestionId == event.gameQuestionId }?.let { question ->
question.userAnswers.filter { userAnswer -> userAnswer.points == null }.forEach {
it.points = 0
}
}

}

fun on(event: UserAddedEvent) {
users.add(GameUserEntity(event.gameUserId, event.username, 0))
users.add(GameUserEntity(event.gameUserId, event.username))
}

fun on(event: UserRemovedEvent) {
Expand All @@ -120,55 +54,11 @@ class GameEntity(
fun on(event: GameCanceledEvent) {
status = GameStatus.CANCELED
}

}

@Entity(name = "GAME_USER")
class GameUserEntity(
@Id
val gameUserId: UUID,
var username: String,
var points: Int
)

@Entity(name = "GAME_QUESTION")
class GameQuestionEntity(
@Id
val gameQuestionId: UUID,
@Enumerated(EnumType.STRING)
val type: QuestionType,
val questionNumber: Int,
val questionPhrase: String,
val questionImagePath: String?,
val questionAsked: Instant,
@Enumerated(EnumType.STRING)
var status: QuestionStatus,
var correctAnswer: String? = null,

@ElementCollection(fetch = FetchType.EAGER)
@Column(name = "answer_option")
@CollectionTable(
name = "GAME_QUESTION_ANSWER_OPTIONS",
joinColumns = [JoinColumn(name = "GAME_QUESTION_ID")]
)
var answerOptions: MutableList<String> = mutableListOf(),

@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
@JoinColumn(name = "game_question_id")
var userAnswers: MutableList<UserAnswerEntity> = mutableListOf()
)

@Entity(name = "USER_ANSWER")
class UserAnswerEntity(
@Id
val userAnswerId: UUID,
val gameUserId: UUID,
var answer: String,
var points: Int? = null
)

enum class QuestionStatus {
OPEN,
CLOSED,
RATED
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ class GameEventHappenedUseCase(

override fun userRemoved(evt: UserRemovedEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { it.on(evt) }

override fun questionAsked(evt: QuestionAskedEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { it.on(evt, metadata.timestamp) }
override fun questionAsked(evt: QuestionAskedEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { }

override fun questionAnswered(evt: QuestionAnsweredEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { it.on(evt) }
override fun questionAnswered(evt: QuestionAnsweredEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { }

override fun questionAnswerOverridden(evt: QuestionAnswerOverriddenEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { it.on(evt) }
override fun questionAnswerOverridden(evt: QuestionAnswerOverriddenEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { }

override fun questionClosed(evt: QuestionClosedEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { it.on(evt) }
override fun questionClosed(evt: QuestionClosedEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { }

override fun questionRated(evt: QuestionRatedEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { it.on(evt) }
override fun questionRated(evt: QuestionRatedEvent, metadata: EventMetaData) = updateAndPropagate(evt, metadata) { }
}
37 changes: 0 additions & 37 deletions backend/src/main/resources/liquibase/db.changelog-master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,43 +28,6 @@
<constraints primaryKey="true" primaryKeyName="GAME_USER_PK" />
</column>
<column name="USERNAME" type="VARCHAR(255)" />
<column name="POINTS" type="NUMBER" />
</createTable>
</changeSet>

<changeSet id="001_create_question" author="thielc">
<createTable tableName="GAME_QUESTION">
<column name="GAME_QUESTION_ID" type="UUID" >
<constraints primaryKey="true" primaryKeyName="GAME_QUESTION_PK" />
</column>
<column name="TYPE" type="VARCHAR(50)" />
<column name="GAME_ID" type="UUID" />
<column name="QUESTION_NUMBER" type="NUMBER" />
<column name="QUESTION_PHRASE" type="VARCHAR(1000)" />
<column name="QUESTION_IMAGE_PATH" type="VARCHAR(1000)" />
<column name="CORRECT_ANSWER" type="VARCHAR(400)" />
<column name="STATUS" type="VARCHAR(100)" />
<column name="QUESTION_ASKED" type="DATETIME" />
</createTable>
</changeSet>

<changeSet id="001_create_question_answer_options" author="thielc">
<createTable tableName="GAME_QUESTION_ANSWER_OPTIONS">
<column name="GAME_QUESTION_ID" type="UUID" />
<column name="ANSWER_OPTION" type="VARCHAR(400)" />
</createTable>
</changeSet>

<changeSet id="001_create_question_user_answer" author="thielc">
<createTable tableName="USER_ANSWER">
<column name="USER_ANSWER_ID" type="UUID" >
<constraints primaryKey="true" primaryKeyName="USER_ANSWER_PK" />
</column>
<column name="GAME_QUESTION_ID" type="UUID" />
<column name="GAME_USER_ID" type="UUID" />
<column name="ANSWER" type="VARCHAR(400)" />
<column name="POINTS" type="NUMBER" />
</createTable>
</changeSet>

</databaseChangeLog>
61 changes: 0 additions & 61 deletions backend/src/test/kotlin/org/quizmania/integration/GameITest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,66 +31,5 @@ class GameITest : AbstractSpringIntegrationTest() {

THEN
.`the game is started`()
.`the game has $ questions`(1)
.`the current question is $ with answer options $`(
questionText = "Was ist gelb und schießt durch den Wald?",
answerOptions = listOf("Banone", "Gürkin", "Nuschel", "Hagenutte")
)
}

@Test
fun `question can be answered`() {
GIVEN
.`a game is created by user $`(USERNAME)
.`the game starts`()

WHEN
.`the user $ answers current question with $`(USERNAME, "Banone")

THEN
.`the question is answered by $`(USERNAME)
.`the last answered question is closed`()
.`user $ scored $ points for the last question`(USERNAME, 10)
}

@Test
fun `multiple users can play game`() {
GIVEN
.`a game is created by user $`(USERNAME)
.`user $ joins the game`(OTHER_USERNAME)
.`the game starts`()

// first question
WHEN
.`the user $ answers current question with $`(USERNAME, "Banone")
.`the user $ answers current question with $`(OTHER_USERNAME, "Gürkin")
THEN
.`the last answered question is closed`()
.`user $ scored $ points for the last question`(USERNAME, 10)
.`user $ scored $ points for the last question`(OTHER_USERNAME, 0)

// open next question
WHEN
.`the next question is asked`()
THEN
.`the game has $ questions`(2)

// third question
WHEN
.`the user $ answers current question with $`(USERNAME, "München")
.`the user $ answers current question with $`(OTHER_USERNAME, "München")

THEN
.`the last answered question is closed`()
.`user $ scored $ points for the last question`(USERNAME, 10)
.`user $ scored $ points for the last question`(USERNAME, 10)
.`user $ scored $ points total`(USERNAME, 20)
.`user $ scored $ points total`(OTHER_USERNAME, 10)

// open next question to end the game
WHEN
.`the next question is asked`(wait = false)
THEN
.`the game is ended`()
}
}
Loading

0 comments on commit 03ccc18

Please sign in to comment.