1
1
package com.squareup.sample.dungeon
2
2
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
3
11
import com.squareup.sample.dungeon.ActorWorkflow.ActorProps
4
12
import com.squareup.sample.dungeon.ActorWorkflow.ActorRendering
5
13
import com.squareup.sample.dungeon.Direction.DOWN
@@ -11,16 +19,13 @@ import com.squareup.sample.dungeon.GameWorkflow.Output
11
19
import com.squareup.sample.dungeon.GameWorkflow.Output.PlayerWasEaten
12
20
import com.squareup.sample.dungeon.GameWorkflow.Output.Vibrate
13
21
import com.squareup.sample.dungeon.GameWorkflow.Props
14
- import com.squareup.sample.dungeon.GameWorkflow.State
15
22
import com.squareup.sample.dungeon.PlayerWorkflow.Rendering
16
23
import com.squareup.sample.dungeon.board.Board
17
24
import com.squareup.sample.dungeon.board.Board.Location
18
- import com.squareup.workflow1.Snapshot
19
- import com.squareup.workflow1.StatefulWorkflow
20
25
import 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
24
29
import com.squareup.workflow1.ui.Screen
25
30
import kotlinx.coroutines.delay
26
31
import kotlinx.coroutines.flow.Flow
@@ -30,11 +35,12 @@ import kotlin.random.Random
30
35
31
36
private val ignoreInput: (Direction ) -> Unit = {}
32
37
38
+ @OptIn(WorkflowExperimentalApi ::class )
33
39
class GameWorkflow (
34
40
private val playerWorkflow : PlayerWorkflow ,
35
41
private val aiWorkflows : List <ActorWorkflow >,
36
42
private val random : Random
37
- ) : StatefulWorkflow <Props, State , Output, GameRendering>() {
43
+ ) : ComposeWorkflow <Props, Output, GameRendering>() {
38
44
39
45
/* *
40
46
* @param board Should not change while the game is running.
@@ -56,9 +62,9 @@ class GameWorkflow(
56
62
/* *
57
63
* Emitted by [GameWorkflow] if the controller should be vibrated.
58
64
*/
59
- object Vibrate : Output()
65
+ data object Vibrate : Output ()
60
66
61
- object PlayerWasEaten : Output()
67
+ data object PlayerWasEaten : Output ()
62
68
}
63
69
64
70
data class GameRendering (
@@ -68,67 +74,68 @@ class GameWorkflow(
68
74
val onStopMoving : (Direction ) -> Unit
69
75
) : Screen
70
76
71
- override fun initialState (
77
+ @Composable
78
+ override fun produceRendering (
72
79
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
+ )
80
90
)
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
+ }
92
92
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
99
94
// 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
104
102
105
103
// Render the player.
106
104
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)
108
107
109
108
// Render all the other actors.
110
109
val aiRenderings = aiWorkflows.zip(game.aiLocations)
111
110
.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
+ }
114
115
}
116
+ val updatedAIR by rememberUpdatedState(aiRenderings)
115
117
116
118
// If the game is paused or finished, just render the board without ticking.
117
119
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
+ }
125
132
}
126
133
}
127
134
128
135
val aiOverlay = aiRenderings.map { (a, b) -> a to b.avatar }
129
136
.toMap()
130
137
val renderedBoard = board.withOverlay(
131
- aiOverlay + (game.playerLocation to playerRendering.actorRendering.avatar)
138
+ aiOverlay + mapOf (game.playerLocation to playerRendering.actorRendering.avatar)
132
139
)
133
140
return GameRendering (
134
141
board = renderedBoard,
@@ -137,17 +144,18 @@ class GameWorkflow(
137
144
)
138
145
}
139
146
140
- override fun snapshotState (state : State ): Snapshot ? = null
141
-
142
147
/* *
143
148
* Calculate new locations for player and other actors.
144
149
*/
145
150
private fun updateGame (
151
+ props : Props ,
152
+ state : State ,
146
153
ticksPerSecond : Int ,
147
154
tick : Long ,
148
155
playerRendering : Rendering ,
149
- aiRenderings : List <Pair <Location , ActorRendering >>
150
- ) = action(" updateGame" ) {
156
+ aiRenderings : List <Pair <Location , ActorRendering >>,
157
+ emitOutput : (Output ) -> Unit
158
+ ): State {
151
159
// Calculate if this tick should result in movement based on the movement's speed.
152
160
fun Movement.isTimeToMove (): Boolean {
153
161
val ticksPerCell = (ticksPerSecond / cellsPerSecond).roundToLong()
@@ -184,12 +192,11 @@ class GameWorkflow(
184
192
185
193
// Check if AI captured player.
186
194
if (newGame.isPlayerEaten) {
187
- state = state.copy(game = newGame)
188
- setOutput(PlayerWasEaten )
195
+ emitOutput(PlayerWasEaten )
189
196
} else {
190
- state = state.copy(game = newGame)
191
- output?.let { setOutput(it) }
197
+ output?.let { emitOutput(it) }
192
198
}
199
+ return state.copy(game = newGame)
193
200
}
194
201
}
195
202
0 commit comments