Skip to content
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 @@ -2,6 +2,7 @@ package com.github.feelbeatapp.androidclient.game.datastreaming

import com.github.feelbeatapp.androidclient.game.model.AudioState
import com.github.feelbeatapp.androidclient.game.model.GameState
import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness
import com.github.feelbeatapp.androidclient.game.model.Player
import com.github.feelbeatapp.androidclient.game.model.RoomStage
import java.time.Duration
Expand Down Expand Up @@ -39,4 +40,74 @@ class Game(private var gameState: GameState) {
fun scheduleAudio(url: String, startAt: Instant, duration: Duration) {
gameState = gameState.copy(audio = AudioState(url, startAt, duration = duration))
}

fun markGuess(id: String) {
gameState =
gameState.copy(
songGuessMap = gameState.songGuessMap.plus(Pair(id, GuessCorrectness.VERIFYING)),
lastGuessStatus = GuessCorrectness.VERIFYING,
)
}

fun resolveGuess(songId: String, correct: Boolean) {
gameState =
gameState.copy(
songGuessMap =
gameState.songGuessMap.plus(
Pair(
songId,
if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT,
)
),
lastGuessStatus =
if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT,
)
}

fun setPlayerGuessResult(playerId: String, correct: Boolean) {
gameState =
gameState.copy(
playerGuessMap =
gameState.playerGuessMap.plus(
Pair(
playerId,
if (correct) GuessCorrectness.CORRECT else GuessCorrectness.INCORRECT,
)
)
)
}

fun addPoints(playerId: String, points: Int) {
gameState =
gameState.copy(
pointsMap =
gameState.pointsMap.plus(
Pair(playerId, gameState.pointsMap.getOrDefault(playerId, 0) + points)
)
)
}

fun setCorrectSong(songId: String) {
gameState =
gameState.copy(
songGuessMap = gameState.songGuessMap.plus(Pair(songId, GuessCorrectness.CORRECT))
)
}

fun resetGuessing() {
gameState = gameState.copy(songGuessMap = mapOf(), playerGuessMap = mapOf())
}

fun resetGame() {
gameState =
gameState.copy(
readyMap = mapOf(),
stage = RoomStage.LOBBY,
audio = null,
pointsMap = mapOf(),
songGuessMap = mapOf(),
playerGuessMap = mapOf(),
lastGuessStatus = GuessCorrectness.UNKNOWN,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.feelbeatapp.androidclient.game.datastreaming

import com.github.feelbeatapp.androidclient.game.model.GameState
import com.github.feelbeatapp.androidclient.game.model.PlayerFinalScore
import com.github.feelbeatapp.androidclient.game.model.RoomSettings
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -12,7 +13,11 @@ interface GameDataStreamer {

fun gameStateFlow(): StateFlow<GameState?>

fun lastGameResultStateFlow(): StateFlow<List<PlayerFinalScore>>

suspend fun updateSettings(settings: RoomSettings)

suspend fun sendReadyStatus(ready: Boolean)

suspend fun sendGuess(id: String, points: Int)
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package com.github.feelbeatapp.androidclient.game.datastreaming

import android.util.Log
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.GuessSongMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.GuessSongPayload
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.ReadyStatusMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.SettingsUpdateMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.client.SettingsUpdatePayload
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.CorrectSongMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.EndGameMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.InitialGameState
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.InitialMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.NewPlayerMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlaySongMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerGuessMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerLeftMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.PlayerReadyMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.RoomStageMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerErrorMessage
import com.github.feelbeatapp.androidclient.game.datastreaming.messages.server.ServerMessageType
import com.github.feelbeatapp.androidclient.game.model.GameState
import com.github.feelbeatapp.androidclient.game.model.GuessCorrectness
import com.github.feelbeatapp.androidclient.game.model.PlayerFinalScore
import com.github.feelbeatapp.androidclient.game.model.RoomSettings
import com.github.feelbeatapp.androidclient.game.model.RoomStage
import com.github.feelbeatapp.androidclient.infra.auth.AuthManager
Expand Down Expand Up @@ -50,6 +57,7 @@ constructor(
) : GameDataStreamer {
private var game: Game? = null
private var gameStateFlow = MutableStateFlow<GameState?>(null)
private var resultStateFlow = MutableStateFlow<List<PlayerFinalScore>>(listOf())
private var scope: CoroutineScope? = null

override suspend fun joinRoom(roomId: String): Deferred<Unit> =
Expand Down Expand Up @@ -94,6 +102,10 @@ constructor(
return gameStateFlow.asStateFlow()
}

override fun lastGameResultStateFlow(): StateFlow<List<PlayerFinalScore>> {
return resultStateFlow.asStateFlow()
}

override suspend fun updateSettings(settings: RoomSettings) {
withContext(Dispatchers.IO) {
val token = authManager.getAccessToken()
Expand All @@ -112,16 +124,51 @@ constructor(
return
}

game
it.setMyReadyStatus(ready)
gameStateFlow.value = it.gameState()
}
}

resultStateFlow.value = listOf()

withContext(Dispatchers.IO) {
networkClient.sendMessage(Json.encodeToString(ReadyStatusMessage(payload = ready)))
}
}

override suspend fun sendGuess(id: String, points: Int) {
synchronized(this) {
game.let {
if (it == null) {
return
}
if (
it.gameState().songGuessMap.getOrDefault(id, GuessCorrectness.UNKNOWN) !=
GuessCorrectness.UNKNOWN
) {
return
}

it.markGuess(id)

if (points == 0) {
it.setPlayerGuessResult(it.gameState().me ?: "", false)
}

gameStateFlow.value = it.gameState()
}
}

withContext(Dispatchers.IO) {
networkClient.sendMessage(
Json.encodeToString(
GuessSongMessage(payload = GuessSongPayload(id = id, points = points))
)
)
}
}

private suspend fun processMessage(content: String) {
try {
val type = Json.decodeFromString<JsonObject>(content)["type"]?.jsonPrimitive?.content
Expand All @@ -135,6 +182,9 @@ constructor(
ServerMessageType.PLAYER_READY.name -> processPlayerReady(content)
ServerMessageType.ROOM_STAGE.name -> processRoomStage(content)
ServerMessageType.PLAY_SONG.name -> processPlaySong(content)
ServerMessageType.PLAYER_GUESS.name -> processPlayerGuess(content)
ServerMessageType.CORRECT_SONG.name -> processCorrectSong(content)
ServerMessageType.END_GAME.name -> processEndGame(content)
else -> Log.w("RemoteGameDataStreamer", "Received unexpected message: $content")
}
} catch (e: Exception) {
Expand Down Expand Up @@ -164,6 +214,10 @@ constructor(
readyMap = initialState.readyMap,
stage = RoomStage.LOBBY,
audio = null,
pointsMap = initialState.players.associateBy({ it.id }, { 0 }),
songGuessMap = mapOf(),
playerGuessMap = mapOf(),
lastGuessStatus = GuessCorrectness.VERIFYING,
)
)
gameStateFlow.value = game?.gameState()
Expand Down Expand Up @@ -207,8 +261,62 @@ constructor(
val start = Instant.ofEpochSecond(payload.timestamp)

synchronized(this) {
game?.scheduleAudio(payload.url, start, Duration.ofMillis(payload.duration))
game.let {
if (it == null) {
return
}

it.scheduleAudio(payload.url, start, Duration.ofMillis(payload.duration))
it.resetGuessing()
gameStateFlow.value = it.gameState()
}
}
}

private fun processPlayerGuess(content: String) {
val payload = Json.decodeFromString<PlayerGuessMessage>(content).payload

synchronized(this) {
game.let {
if (game == null) {
return
}

if (payload.songId.isNotEmpty()) {
it?.resolveGuess(payload.songId, payload.correct)
}

it?.setPlayerGuessResult(payload.playerId, payload.correct)
it?.addPoints(payload.playerId, payload.points)

gameStateFlow.value = it?.gameState()
}
}
}

private fun processCorrectSong(content: String) {
val correctSongId = Json.decodeFromString<CorrectSongMessage>(content).payload

synchronized(this) {
game?.setCorrectSong(correctSongId)
gameStateFlow.value = game?.gameState()
}
}

private fun processEndGame(content: String) {
val payload = Json.decodeFromString<EndGameMessage>(content).payload

synchronized(this) {
game.let {
if (it == null) {
return
}

it.resetGame()
gameStateFlow.value = it.gameState()
}
}

resultStateFlow.value = payload.results
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client
enum class ClientMessageType {
SETTINGS_UPDATE,
READY_STATUS,
GUESS_SONG,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.client

import kotlinx.serialization.Required
import kotlinx.serialization.Serializable

@Serializable
data class GuessSongMessage(
@Required val type: String = ClientMessageType.GUESS_SONG.name,
val payload: GuessSongPayload,
)

@Serializable data class GuessSongPayload(val id: String, val points: Int)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server

import kotlinx.serialization.Serializable

@Serializable
data class CorrectSongMessage(
override val type: String = ServerMessageType.CORRECT_SONG.name,
val payload: String,
) : ServerMessage()
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server

import com.github.feelbeatapp.androidclient.game.model.PlayerFinalScore
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@SerialName("END_GAME")
data class EndGameMessage(
override val type: String = ServerMessageType.END_GAME.name,
val payload: EndGamePayload,
) : ServerMessage()

@Serializable data class EndGamePayload(val results: List<PlayerFinalScore>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.github.feelbeatapp.androidclient.game.datastreaming.messages.server

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@SerialName("PLAYER_GUESS")
data class PlayerGuessMessage(
override val type: String = ServerMessageType.PLAYER_GUESS.name,
val payload: PlayerGuessPayload,
) : ServerMessage()

@Serializable
data class PlayerGuessPayload(
val correct: Boolean,
val points: Int,
val playerId: String,
val songId: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ enum class ServerMessageType {
PLAYER_READY,
ROOM_STAGE,
PLAY_SONG,
PLAYER_GUESS,
CORRECT_SONG,
END_GAME,
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ data class GameState(
val readyMap: Map<String, Boolean>,
val stage: RoomStage,
val audio: AudioState?,
val pointsMap: Map<String, Int>,
val songGuessMap: Map<String, GuessCorrectness>,
val playerGuessMap: Map<String, GuessCorrectness>,
val lastGuessStatus: GuessCorrectness,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.feelbeatapp.androidclient.game.model

enum class GuessCorrectness {
UNKNOWN,
VERIFYING,
CORRECT,
INCORRECT,
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package com.github.feelbeatapp.androidclient.game.model

import kotlinx.serialization.Serializable

@Serializable data class Player(val id: String, val name: String, val imageUrl: String, val score: Int = 0)
@Serializable data class Player(val id: String, val name: String, val imageUrl: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.github.feelbeatapp.androidclient.game.model

import kotlinx.serialization.Serializable

@Serializable
data class PlayerFinalScore(
val profile: Player ,
val points: Int
)
Loading
Loading