11package com.squareup.sample.dungeon
22
3+ import androidx.compose.runtime.Composable
4+ import androidx.compose.runtime.LaunchedEffect
5+ import androidx.compose.runtime.getValue
6+ import androidx.compose.runtime.key
7+ import androidx.compose.runtime.mutableStateOf
8+ import androidx.compose.runtime.remember
9+ import androidx.compose.runtime.rememberUpdatedState
10+ import androidx.compose.runtime.setValue
311import com.squareup.sample.dungeon.ActorWorkflow.ActorProps
412import com.squareup.sample.dungeon.ActorWorkflow.ActorRendering
513import com.squareup.sample.dungeon.Direction.DOWN
@@ -11,16 +19,13 @@ import com.squareup.sample.dungeon.GameWorkflow.Output
1119import com.squareup.sample.dungeon.GameWorkflow.Output.PlayerWasEaten
1220import com.squareup.sample.dungeon.GameWorkflow.Output.Vibrate
1321import com.squareup.sample.dungeon.GameWorkflow.Props
14- import com.squareup.sample.dungeon.GameWorkflow.State
1522import com.squareup.sample.dungeon.PlayerWorkflow.Rendering
1623import com.squareup.sample.dungeon.board.Board
1724import com.squareup.sample.dungeon.board.Board.Location
18- import com.squareup.workflow1.Snapshot
19- import com.squareup.workflow1.StatefulWorkflow
2025import com.squareup.workflow1.Worker
21- import com.squareup.workflow1.action
22- import com.squareup.workflow1.renderChild
23- import com.squareup.workflow1.runningWorker
26+ import com.squareup.workflow1.WorkflowExperimentalApi
27+ import com.squareup.workflow1.compose.ComposeWorkflow
28+ import com.squareup.workflow1.compose.renderChild
2429import com.squareup.workflow1.ui.Screen
2530import kotlinx.coroutines.delay
2631import kotlinx.coroutines.flow.Flow
@@ -30,11 +35,12 @@ import kotlin.random.Random
3035
3136private val ignoreInput: (Direction ) -> Unit = {}
3237
38+ @OptIn(WorkflowExperimentalApi ::class )
3339class GameWorkflow (
3440 private val playerWorkflow : PlayerWorkflow ,
3541 private val aiWorkflows : List <ActorWorkflow >,
3642 private val random : Random
37- ) : StatefulWorkflow <Props, State , Output, GameRendering>() {
43+ ) : ComposeWorkflow <Props, Output, GameRendering>() {
3844
3945 /* *
4046 * @param board Should not change while the game is running.
@@ -56,9 +62,9 @@ class GameWorkflow(
5662 /* *
5763 * Emitted by [GameWorkflow] if the controller should be vibrated.
5864 */
59- object Vibrate : Output()
65+ data object Vibrate : Output ()
6066
61- object PlayerWasEaten : Output()
67+ data object PlayerWasEaten : Output ()
6268 }
6369
6470 data class GameRendering (
@@ -68,67 +74,68 @@ class GameWorkflow(
6874 val onStopMoving : (Direction ) -> Unit
6975 ) : Screen
7076
71- override fun initialState (
77+ @Composable
78+ override fun produceRendering (
7279 props : Props ,
73- snapshot : Snapshot ?
74- ): State {
75- val board = props.board
76- return State (
77- game = Game (
78- playerLocation = random.nextEmptyLocation(board),
79- aiLocations = aiWorkflows.map { random.nextEmptyLocation(board) }
80+ emitOutput : (Output ) -> Unit
81+ ): GameRendering {
82+ var state by remember {
83+ mutableStateOf(
84+ State (
85+ game = Game (
86+ playerLocation = random.nextEmptyLocation(props.board),
87+ aiLocations = aiWorkflows.map { random.nextEmptyLocation(props.board) }
88+ )
89+ )
8090 )
81- )
82- }
83-
84- override fun onPropsChanged (
85- old : Props ,
86- new : Props ,
87- state : State
88- ): State {
89- check(old.board == new.board) { " Expected board to not change during the game." }
90- return state
91- }
91+ }
9292
93- override fun render (
94- renderProps : Props ,
95- renderState : State ,
96- context : RenderContext <Props , State , Output >
97- ): GameRendering {
98- val running = ! renderProps.paused && ! renderState.game.isPlayerEaten
93+ val running = ! props.paused && ! state.game.isPlayerEaten
9994 // Stop actors from ticking if the game is paused or finished.
100- val ticker: Worker <Long > =
101- if (running) TickerWorker (renderProps.ticksPerSecond) else Worker .finished()
102- val game = renderState.game
103- val board = renderProps.board
95+ val ticker: Worker <Long > = if (running) {
96+ remember { TickerWorker (props.ticksPerSecond) }
97+ } else {
98+ Worker .finished()
99+ }
100+ val game = state.game
101+ val board = props.board
104102
105103 // Render the player.
106104 val playerInput = ActorProps (board, game.playerLocation, ticker)
107- val playerRendering = context.renderChild(playerWorkflow, playerInput)
105+ val playerRendering = renderChild(playerWorkflow, playerInput)
106+ val updatedPR by rememberUpdatedState(playerRendering)
108107
109108 // Render all the other actors.
110109 val aiRenderings = aiWorkflows.zip(game.aiLocations)
111110 .mapIndexed { index, (aiWorkflow, aiLocation) ->
112- val aiInput = ActorProps (board, aiLocation, ticker)
113- aiLocation to context.renderChild(aiWorkflow, aiInput, key = index.toString())
111+ key(index) {
112+ val aiInput = ActorProps (board, aiLocation, ticker)
113+ aiLocation to renderChild(aiWorkflow, aiInput)
114+ }
114115 }
116+ val updatedAIR by rememberUpdatedState(aiRenderings)
115117
116118 // If the game is paused or finished, just render the board without ticking.
117119 if (running) {
118- context.runningWorker(ticker) { tick ->
119- return @runningWorker updateGame(
120- renderProps.ticksPerSecond,
121- tick,
122- playerRendering,
123- aiRenderings
124- )
120+ LaunchedEffect (ticker) {
121+ ticker.run ().collect { tick ->
122+ state = updateGame(
123+ props,
124+ state,
125+ props.ticksPerSecond,
126+ tick,
127+ updatedPR,
128+ updatedAIR,
129+ emitOutput
130+ )
131+ }
125132 }
126133 }
127134
128135 val aiOverlay = aiRenderings.map { (a, b) -> a to b.avatar }
129136 .toMap()
130137 val renderedBoard = board.withOverlay(
131- aiOverlay + (game.playerLocation to playerRendering.actorRendering.avatar)
138+ aiOverlay + mapOf (game.playerLocation to playerRendering.actorRendering.avatar)
132139 )
133140 return GameRendering (
134141 board = renderedBoard,
@@ -137,17 +144,18 @@ class GameWorkflow(
137144 )
138145 }
139146
140- override fun snapshotState (state : State ): Snapshot ? = null
141-
142147 /* *
143148 * Calculate new locations for player and other actors.
144149 */
145150 private fun updateGame (
151+ props : Props ,
152+ state : State ,
146153 ticksPerSecond : Int ,
147154 tick : Long ,
148155 playerRendering : Rendering ,
149- aiRenderings : List <Pair <Location , ActorRendering >>
150- ) = action(" updateGame" ) {
156+ aiRenderings : List <Pair <Location , ActorRendering >>,
157+ emitOutput : (Output ) -> Unit
158+ ): State {
151159 // Calculate if this tick should result in movement based on the movement's speed.
152160 fun Movement.isTimeToMove (): Boolean {
153161 val ticksPerCell = (ticksPerSecond / cellsPerSecond).roundToLong()
@@ -184,12 +192,11 @@ class GameWorkflow(
184192
185193 // Check if AI captured player.
186194 if (newGame.isPlayerEaten) {
187- state = state.copy(game = newGame)
188- setOutput(PlayerWasEaten )
195+ emitOutput(PlayerWasEaten )
189196 } else {
190- state = state.copy(game = newGame)
191- output?.let { setOutput(it) }
197+ output?.let { emitOutput(it) }
192198 }
199+ return state.copy(game = newGame)
193200 }
194201}
195202
0 commit comments