Continuing from the previous article - Battleship game in Kotlin, we will use Redux architecture to write the gameplay. Redux is a predictable state container with unidirectional data flow. As we write our reducers and middleware, you’ll realize what predictable state container brings to the table. It makes everything straight forward and easy to debug.
We’ll use the Redux Store implementation that we have been discussing in this series. Redux has a store, a state tree, actions, reducers and middleware. Let’s start with the state.
The state tree should store all the necessary data.
Each Battleship game has 2 players so we add 2 boards to our game state tree.
lastPlayed is the unique id of the board which had the last turn. This would help us in determining who is going to play next.
gameOver is a flag which would stop the game and declare a winner.
Let’s define some actions. The user can take only one action =>
take a shot. To keep things clear, we’ll add
defense board id with every action.
When a user takes a shot, there are number of possibilities that can happen.
- The user has already taken a shot at that point, so it's not a valid move.
- The user takes a shot, but misses.
- The user takes a shot and hits a ship.
- The ship is not completely destroyed.
- The ship is completely destroyed.
- Not all the ships of the defense have been destroyed.
- All the ships of the defense have been destroyed and the offense wins the game.
Middleware to handle these logical possibilities. It means that middleware will take care of ouse business logic. A middleware can not update the state. Only a reducer can update the state when an action is dispatched.
Let’s create some actions that our middleware will dispatch based on the business logic.
Let’s go over the actions briefly.
Game setup: Add ships on the board before the game begins.
AddShip- User generated action to add ship on the board.
AddShipInvalid- System generated action which says the ship can’t be added for some reason, eg. the ship doesn’t fit on the board, the ship overlaps already placed ship, etc.
System generated actions:
InvalidMove- The move is not valid. The reducer will update state accordingly.
PlayMove- The move is valid.
MissedMove- The shot did not hit any of the ships.
Definitive actions: shot hit a ship.
HitMove- The shot hit a ship.
DestroyShip- The shot hit a ship and destroyed it.
LostGame- The shot hit a ship, destroyed it and the game is over.
SwitchAction- This action signifies change in turn. After player 1 take a shot, if it’s valid, this action will be dispatched at the end which will update the state and ask the 2nd player to take a shot. It wraps a
GeneratedActionso that the reducer can update the state based on the action.
InvalidState- This action signifies that values in action and state do not match. Eg.
defenseid are different than the board ids in the state.
Let’s write our Middleware functions. We will take advantage of the Middleware chaining.
Let’s write a middleware function which will check if the
defense board id in the action correspond to the boards in the state or not. If not, it will return
InvalidState and not call the chain further.
Once the action passes through the state validation, we can be assured that the state is correct.
We will write a middleware to setup the game. It will intercept
AddShip action and apply business logic on it.
This middleware checks if the ship fits on the board and it does not overlap any other ship on the board. If it fails the above conditions, the middleware propagates
AddShipInvalid action to the chain.
Let’s write a middleware to check validity of the move. It will check if the shot at that grid has already been taken or not.
If the shot is already taken at the grid, this middleware calls the chain with
InvalidMove action. We can chose to return the action here itself.
It calls the chain with
PlayMove action if the move can be taken.
We will write a middleware which will check if the shot hit a ship or missed.
If the shot hits a ship, we call the middleware chain with
HitMove or else we call the chain with
Destroy and Lost Middleware
Similarly, let’s write a middleware which checks if the hit ship is destroyed and another middleware to check if the defense lost the game.
HitMove action and checks if the ship will be destroyed or not and propagates
DestroyShip action and checks if all the ships would have been destroyed or not and propagates
Switch turns Middleware
After we have determined the action that should be dispatched, we wrap it in
SwitchAction. The reducer, upon receiving this action, will update the state based on the wrapped action and update the state again which will indicate change of turns.
We are done with the business logic and middleware. Let’s focus on writing the reducers now.
A reducer is a pure function which reduces (changes) the state based on the dispatched action.
Before writing reducers for
GameState, let’s break it down and write reducers for
A board reducer will reduce
Board state based on the action. It will not update the
GameState directly. The
GameState reducer will call Board reducers to update the state.
We write two different reducers for
reduceOffenseis used to reduce the board which played the current turn.
reduceDefenseis used to reduce the other board which took the shot.
reduceSetupis used to reduce the board for setup actions.
Board.reduceOffense is an extension function and we can access the board instance by
this. It is similar to writing
fun reduceOffense(board: Board).
This reducer only reduce the board for
AddShip action. For other actions, it will return the state as is.
This reducer will take the action and reduce the board considering the board is the offense and it took the shot. If the action is
MissedMove, it will add the point to
misses and not
DefinitiveAction, it will add the point to
hits and not try to reduce the ships.
This reducer will reduce the state assuming the shot was taken on this board. For
DefinitiveAction, it will add the point to
opponentHits and reduce the ship also.
We have covered all the actions that should update the boards. Let’s write reducers for
These reducers will reduce
GameState while also reducing the child states in the state tree.
We write one reducer for the game setup and another one for the gameplay. Each of the reducers use
reduceChildState function to reduce sub states.
It’s a function which lets you reduce a sub-state (child state) of your state with the provided sub-state reducer. Traditionally, in Redux, every reducer creates a new instance of the state, regardless of the change. In Java/Kotlin, creating new instances will result into unnecessary memory allocations and we all know how the garbage collector is. So we try to avoid creating new states as much as possible.
This function reduces the child state. It checks the references of the current child state and update child state via
===. If the references are same, it means that there was no change in the state and it returns the state as is.
If there’s a change in the child state, it will invoke
onReduced function which is supposed to update the state with child state.
This reducer updates the state only for
AddShip action. It uses
reduceChildState function to reduce the board using
This reducer reacts to
SwitchAction and reduces
GameState based on the actions. When
SwitchAction is dispatched, it reduces the state based on the wrapped action and updates
GeneratedAction, the reducer first reduces the offense board and then the defense board and update the state by copying.
whichBoard is just a convenient method which returns the new board based on the old board id.
We have defined our state, actions, reducers and middleware. Let’s define our store and create a view which would listen to the updates.
Generally, an app based on Redux has only one store. We are also going to use only one store.
SimpleStore which we have defined in the previous articles. We supply the list of middleware and directly used reducers. When we create an instance of the store, we’ll just need to pass an initial state as constructor parameter.
Render and Updates
We have completed the Redux implementation for our Battleship game. We need to write some
render code which will render the views on the screen based on the state updates from the store.
Without making things too complicated, let’s just update the activity. You can find the layout here - acitivty_game.xml.
onCreate, we initialize the views and create empty boards. Using these boards, we create an initial state and
You can ask the user to setup ships, but here we are just going to put the ships on the board
randomly (It’s not random). To put ship, we’ll dispatch
We subscribe to the store for state updates in
onResume and write our render function. For every update, it will recreate the cells and update the adapter. We unsubscribe from the updates in
onPause lifecycle callback.
To take a shot, we dispatch
Move action. The instance of action has
defense id and the point where the user clicked.
This is it! You have created the game of Battleship using Kotlin and Redux architecture. Start playing!
I have uploaded the code on Github and you may find it here - Battleship.
This article series has been the longest I have worked on and frankly, quite time consuming. I hope you liked the content and found it useful. Please share it with your colleagues and in your community if they are interested in learning about Redux.
Redux architecture series
- Introduction: Redux architecture for android apps
- Middleware: Introduction and implementation
- Write your own Redux implementation in Kotlin
- Add Middleware to your Redux implementation
- Build Battleship game with Redux - Groundwork
- Implement the Battleship gameplay with Redux