summaryrefslogtreecommitdiff
path: root/sw-ui-kt/src/main/kotlin/org/luxons
diff options
context:
space:
mode:
Diffstat (limited to 'sw-ui-kt/src/main/kotlin/org/luxons')
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt17
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt4
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt27
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt19
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt2
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt14
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt52
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt15
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt33
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt20
-rw-r--r--sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt143
11 files changed, 335 insertions, 11 deletions
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt
index 2a075ba9..fafa2333 100644
--- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt
@@ -1,11 +1,16 @@
package org.luxons.sevenwonders.ui
import org.luxons.sevenwonders.ui.components.application
+import org.luxons.sevenwonders.ui.redux.SwState
import org.luxons.sevenwonders.ui.redux.configureStore
+import org.luxons.sevenwonders.ui.redux.sagas.SagaManager
+import org.luxons.sevenwonders.ui.redux.sagas.rootSaga
import org.w3c.dom.Element
-import react.RBuilder
import react.dom.*
import react.redux.provider
+import redux.RAction
+import redux.Store
+import redux.WrapperAction
import kotlin.browser.document
import kotlin.browser.window
@@ -21,10 +26,18 @@ fun main() {
}
private fun initializeAndRender(rootElement: Element) {
- val store = configureStore()
+ val store = initRedux()
+
render(rootElement) {
provider(store) {
application()
}
}
}
+
+private fun initRedux(): Store<SwState, RAction, WrapperAction> {
+ val sagaManager = SagaManager<SwState, RAction, WrapperAction>()
+ val store = configureStore(sagaManager = sagaManager)
+ sagaManager.startSaga(rootSaga())
+ return store
+}
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt
index 8bf43f1d..30a0883a 100644
--- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt
@@ -3,7 +3,7 @@ package org.luxons.sevenwonders.ui.components.home
import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onSubmitFunction
-import org.luxons.sevenwonders.ui.redux.ChooseUserName
+import org.luxons.sevenwonders.ui.redux.RequestChooseName
import org.luxons.sevenwonders.ui.redux.connectDispatch
import react.RBuilder
import react.RClass
@@ -37,5 +37,5 @@ private class ChooseNameForm(props: ChooseNameFormProps): RComponent<ChooseNameF
}
val chooseNameForm: RClass<RProps> = connectDispatch(ChooseNameForm::class) { dispatch, _ ->
- chooseUsername = { name -> dispatch(ChooseUserName(name)) }
+ chooseUsername = { name -> dispatch(RequestChooseName(name)) }
}
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt
index 3fc51d6e..27c451be 100644
--- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Actions.kt
@@ -1,5 +1,30 @@
package org.luxons.sevenwonders.ui.redux
+import org.luxons.sevenwonders.model.GameState
+import org.luxons.sevenwonders.model.PlayerMove
+import org.luxons.sevenwonders.model.api.LobbyDTO
+import org.luxons.sevenwonders.model.api.PlayerDTO
+import org.luxons.sevenwonders.model.cards.PreparedCard
import redux.RAction
-class ChooseUserName(val newUsername: String): RAction
+data class SetCurrentPlayerAction(val player: PlayerDTO): RAction
+
+data class UpdateGameListAction(val games: List<LobbyDTO>): RAction
+
+data class UpdateLobbyAction(val lobby: LobbyDTO): RAction
+
+data class EnterLobbyAction(val gameId: Long): RAction
+
+data class UpdatePlayers(val players: Map<String, PlayerDTO>): RAction
+
+data class EnterGameAction(val gameId: Long): RAction
+
+data class TurnInfoEvent(val players: Map<String, PlayerDTO>): RAction
+
+data class PrepareMoveAction(val move: PlayerMove): RAction
+
+data class PreparedCardEvent(val card: PreparedCard): RAction
+
+data class PlayerReadyEvent(val username: String): RAction
+
+data class TableUpdateEvent(val table: GameState): RAction
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt
new file mode 100644
index 00000000..dc959a2f
--- /dev/null
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/ApiActions.kt
@@ -0,0 +1,19 @@
+package org.luxons.sevenwonders.ui.redux
+
+import org.luxons.sevenwonders.model.CustomizableSettings
+import org.luxons.sevenwonders.model.PlayerMove
+import redux.RAction
+
+data class RequestChooseName(val playerName: String): RAction
+
+data class RequestCreateGameAction(val gameName: String): RAction
+
+data class RequestJoinGameAction(val gameId: Long): RAction
+
+data class RequestReorderPlayers(val orderedPlayers: List<String>): RAction
+
+data class RequestUpdateSettings(val settings: CustomizableSettings): RAction
+
+class RequestStartGameAction: RAction
+
+data class PrepareMove(val move: PlayerMove): RAction
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt
index d811d32e..f9f5a26d 100644
--- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Reducers.kt
@@ -3,6 +3,6 @@ package org.luxons.sevenwonders.ui.redux
import redux.RAction
fun rootReducer(state: SwState, action: RAction) = when (action) {
- is ChooseUserName -> state.copy(playerName = action.newUsername)
+ is RequestChooseName -> state.copy(playerName = action.playerName)
else -> state
}
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt
index 75f7e8a8..fea13f04 100644
--- a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Store.kt
@@ -1,8 +1,11 @@
package org.luxons.sevenwonders.ui.redux
+import org.luxons.sevenwonders.ui.redux.sagas.SagaManager
import redux.RAction
import redux.Store
import redux.WrapperAction
+import redux.applyMiddleware
+import redux.compose
import redux.createStore
import redux.rEnhancer
@@ -12,9 +15,10 @@ data class SwState(
val INITIAL_STATE = SwState("Bob")
-fun configureStore(initialState: SwState = INITIAL_STATE): Store<SwState, RAction, WrapperAction> {
-
- // TODO configure middlewares
-
- return createStore(::rootReducer, initialState, rEnhancer())
+fun configureStore(
+ sagaManager: SagaManager<SwState, RAction, WrapperAction>,
+ initialState: SwState = INITIAL_STATE
+): Store<SwState, RAction, WrapperAction> {
+ val sagaEnhancer = applyMiddleware(sagaManager.createMiddleware())
+ return createStore(::rootReducer, initialState, compose(sagaEnhancer, rEnhancer()))
}
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt
new file mode 100644
index 00000000..cd9e0cf8
--- /dev/null
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameBrowserSagas.kt
@@ -0,0 +1,52 @@
+package org.luxons.sevenwonders.ui.redux.sagas
+
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.isActive
+import org.luxons.sevenwonders.client.SevenWondersSession
+import org.luxons.sevenwonders.model.api.LobbyDTO
+import org.luxons.sevenwonders.ui.redux.EnterLobbyAction
+import org.luxons.sevenwonders.ui.redux.RequestCreateGameAction
+import org.luxons.sevenwonders.ui.redux.RequestJoinGameAction
+import org.luxons.sevenwonders.ui.redux.SwState
+import org.luxons.sevenwonders.ui.redux.UpdateGameListAction
+import org.luxons.sevenwonders.ui.redux.UpdateLobbyAction
+import redux.RAction
+import redux.WrapperAction
+
+fun gameBrowserSaga(session: SevenWondersSession) = saga<SwState, RAction, WrapperAction> {
+ val watchGamesJob = fork(watchGames(session))
+ val watchCreateGameJob = fork(watchCreateGame(session))
+ val watchJoinGameJob = fork(watchJoinGame(session))
+}
+
+private fun watchGames(session: SevenWondersSession) = saga<SwState, RAction, WrapperAction> {
+ val gamesSubscription = session.watchGames()
+ for (lobbies in gamesSubscription.messages) {
+ if (!isActive) {
+ gamesSubscription.unsubscribe()
+ break
+ }
+ dispatch(UpdateGameListAction(lobbies.toList()))
+ }
+}
+
+private fun watchCreateGame(session: SevenWondersSession) =
+ actionHandlerSaga<SwState, RAction, WrapperAction, RequestCreateGameAction> {
+ val lobby = session.createGame(it.gameName)
+ handleGameJoined(session, lobby)
+ }
+
+private fun watchJoinGame(session: SevenWondersSession) = actionHandlerSaga<SwState, RAction, WrapperAction, RequestJoinGameAction> {
+ val lobby = session.joinGame(it.gameId)
+ handleGameJoined(session, lobby)
+}
+
+private fun SagaContext<SwState, RAction, WrapperAction>.handleGameJoined(
+ session: SevenWondersSession,
+ lobby: LobbyDTO
+) {
+ dispatch(UpdateLobbyAction(lobby))
+ dispatch(EnterLobbyAction(lobby.id))
+ fork(lobbySaga(session, lobby.id))
+ // TODO push /lobby/{lobby.id}
+}
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt
new file mode 100644
index 00000000..24a9e1b4
--- /dev/null
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/GameSagas.kt
@@ -0,0 +1,15 @@
+package org.luxons.sevenwonders.ui.redux.sagas
+
+import org.luxons.sevenwonders.client.SevenWondersSession
+import org.luxons.sevenwonders.ui.redux.SwState
+import redux.RAction
+import redux.WrapperAction
+
+fun gameSaga(session: SevenWondersSession, gameId: Long) = saga<SwState, RAction, WrapperAction> {
+ fork(watchPlayerReady(session, gameId))
+}
+
+fun watchPlayerReady(session: SevenWondersSession, gameId: Long) = saga<SwState, RAction, WrapperAction> {
+ session.watchPlayerReady(gameId)
+}
+
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt
new file mode 100644
index 00000000..89e8e890
--- /dev/null
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/LobbySagas.kt
@@ -0,0 +1,33 @@
+package org.luxons.sevenwonders.ui.redux.sagas
+
+import org.luxons.sevenwonders.client.SevenWondersSession
+import org.luxons.sevenwonders.ui.redux.EnterGameAction
+import org.luxons.sevenwonders.ui.redux.RequestStartGameAction
+import org.luxons.sevenwonders.ui.redux.SwState
+import org.luxons.sevenwonders.ui.redux.UpdateLobbyAction
+import redux.RAction
+import redux.WrapperAction
+
+fun lobbySaga(session: SevenWondersSession, lobbyId: Long) = saga<SwState, RAction, WrapperAction> {
+ fork(watchLobbyUpdates(session, lobbyId))
+ fork(watchGameStart(session, lobbyId))
+ fork(startGame(session))
+}
+
+private fun watchLobbyUpdates(session: SevenWondersSession, lobbyId: Long) = saga<SwState, RAction, WrapperAction> {
+ val lobbyUpdates = session.watchLobbyUpdates(lobbyId)
+ for (lobby in lobbyUpdates.messages) {
+ dispatch(UpdateLobbyAction(lobby))
+ }
+}
+
+private fun watchGameStart(session: SevenWondersSession, lobbyId: Long) = saga<SwState, RAction, WrapperAction> {
+ val gameStartSubscription = session.watchGameStart(lobbyId)
+ gameStartSubscription.messages.receive()
+ dispatch(EnterGameAction(lobbyId))
+ // TODO push /game/{lobby.id}
+}
+
+private fun startGame(session: SevenWondersSession) = actionHandlerSaga<SwState, RAction, WrapperAction, RequestStartGameAction> {
+ session.startGame()
+}
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt
new file mode 100644
index 00000000..96311f98
--- /dev/null
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/Sagas.kt
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.ui.redux.sagas
+
+import org.luxons.sevenwonders.client.SevenWondersClient
+import org.luxons.sevenwonders.model.api.SEVEN_WONDERS_WS_ENDPOINT
+import org.luxons.sevenwonders.ui.redux.RequestChooseName
+import org.luxons.sevenwonders.ui.redux.SetCurrentPlayerAction
+import org.luxons.sevenwonders.ui.redux.SwState
+import redux.RAction
+import redux.WrapperAction
+
+fun rootSaga() = saga<SwState, RAction, WrapperAction> {
+ val action = next<RequestChooseName>()
+ val session = SevenWondersClient().connect(SEVEN_WONDERS_WS_ENDPOINT)
+
+ fork(gameBrowserSaga(session))
+
+ val player = session.chooseName(action.playerName)
+ dispatch(SetCurrentPlayerAction(player))
+ // push /games
+}
diff --git a/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt
new file mode 100644
index 00000000..2aa677fe
--- /dev/null
+++ b/sw-ui-kt/src/main/kotlin/org/luxons/sevenwonders/ui/redux/sagas/SagasFramework.kt
@@ -0,0 +1,143 @@
+package org.luxons.sevenwonders.ui.redux.sagas
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.BroadcastChannel
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.promise
+import redux.Middleware
+import redux.MiddlewareApi
+import redux.RAction
+import kotlin.coroutines.CoroutineContext
+
+@UseExperimental(ExperimentalCoroutinesApi::class)
+class SagaManager<S, A : RAction, R>(
+ private val monitor: ((A) -> Unit)? = null
+) {
+ private lateinit var context: SagaContext<S, A, R>
+
+ private val actions = BroadcastChannel<A>(16)
+
+ fun createMiddleware(): Middleware<S, A, R, A, R> = ::sagasMiddleware
+
+ private fun sagasMiddleware(api: MiddlewareApi<S, A, R>): ((A) -> R) -> (A) -> R {
+ context = SagaContext(api, actions)
+ return { nextDispatch ->
+ { action ->
+ onActionDispatched(action)
+ val result = nextDispatch(action)
+ handleAction(action)
+ result
+ }
+ }
+ }
+
+ private fun onActionDispatched(action: A) {
+ monitor?.invoke(action)
+ }
+
+ private fun handleAction(action: A) {
+ GlobalScope.promise { actions.send(action) }
+ }
+
+ fun startSaga(saga: Saga<S, A, R>): Job {
+ check(::context.isInitialized) {
+ "Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware"
+ }
+ return context.launch {
+ saga.execute(context)
+ }
+ }
+
+ suspend fun runSaga(saga: Saga<S, A, R>) {
+ check(::context.isInitialized) {
+ "Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware"
+ }
+ saga.execute(context)
+ }
+}
+
+fun <S, A : RAction, R> saga(block: suspend SagaContext<S, A, R>.() -> Unit) = Saga(block)
+
+inline fun <S, A : RAction, R, reified AT : A> actionHandlerSaga(
+ noinline block: suspend SagaContext<S, A, R>.(AT) -> Unit
+) = saga<S, A, R> {
+ onEach<AT> {
+ block(it)
+ }
+}
+
+class Saga<S, A : RAction, R>(private val body: suspend SagaContext<S, A, R>.() -> Unit) {
+
+ internal suspend fun execute(context: SagaContext<S, A, R>) = context.body()
+}
+
+@UseExperimental(FlowPreview::class, ExperimentalCoroutinesApi::class)
+class SagaContext<S, A : RAction, R>(
+ private val reduxApi: MiddlewareApi<S, A, R>,
+ private val actions: BroadcastChannel<A>
+): CoroutineScope {
+
+ private val job = Job()
+
+ override val coroutineContext: CoroutineContext
+ get() = job
+
+ fun dispatch(action: A) {
+ reduxApi.dispatch(action)
+ }
+
+ /**
+ * Starts a concurrent coroutine that executes [handle] on every action dispatched.
+ * Returns an "unsubscribe" function.
+ */
+ suspend fun onEach(handle: suspend SagaContext<S, A, R>.(A) -> Unit) {
+ val channel = actions.openSubscription()
+ for (a in channel) {
+ if (!isActive) {
+ channel.cancel()
+ break
+ }
+ handle(a)
+ }
+ }
+
+ /**
+ * Starts a concurrent coroutine that executes [handle] on every action dispatched of the given type.
+ * Returns an "unsubscribe" function.
+ */
+ suspend inline fun <reified AT : A> onEach(
+ crossinline handle: suspend SagaContext<S, A, R>.(AT) -> Unit
+ ) = onEach {
+ if (it is AT) {
+ handle(it)
+ }
+ }
+
+ /**
+ * Suspends until the next action matching the given [predicate] is dispatched, and returns that action.
+ */
+ suspend fun next(predicate: (A) -> Boolean): A {
+ val channel = actions.openSubscription()
+ for (a in channel) {
+ if (predicate(a)) {
+ channel.cancel()
+ return a
+ }
+ }
+ throw IllegalStateException("Actions channel closed before receiving a matching action")
+ }
+
+ /**
+ * Suspends until the next action of type [T] is dispatched, and returns that action.
+ */
+ suspend inline fun <reified T : A> next(): T = next { it is T } as T
+
+ fun fork(saga: Saga<S, A, R>) = SagaContext(reduxApi, actions).launch {
+ saga.execute(this@SagaContext)
+ }
+}
bgstack15