Moving checkers men (functional programming)
In this article I explain how to implement moving checkers pieces using only immutable lists.
Project
The project I use for the demonstation is Checkers.
Pending
Only movements of white men are implemented. In future versions, black men are going to be moved by the computer.
Kings are not supported. In memory, position of kings is represented by empty lists.
Layout
This is the layout file:
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="pl.org.seva.checkers.game.GameVM"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<pl.org.seva.checkers.game.view.Board
android:id="@+id/board"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<pl.org.seva.checkers.game.view.Pieces
android:id="@+id/pieces"
android:layout_width="match_parent"
android:layout_height="match_parent"
gameState="@{viewModel.gameStateFlow}"/>
</RelativeLayout>
</layout>
I use two views in my game, separately for the board and for pieces.
Notice the line gameState="@{viewModel.gameStateFlow}
. It binds the view with the StateFlow
containing information about positions of the pieces.
Data class
This is the data class containing positions of men and kings:
data class GameState(
val whiteMen: List<Pair<Int, Int>>,
val blackMen: List<Pair<Int, Int>>,
val whiteKings: List<Pair<Int, Int>>,
val blackKings: List<Pair<Int, Int>>,
val moving: Pair<Int, Int> = -1 to -1,
) {
fun containsWhite(pair: Pair<Int, Int>) = whiteMen.contains(pair) || whiteKings.contains(pair)
fun containsBlack(pair: Pair<Int, Int>) = blackMen.contains(pair) || whiteKings.contains(pair)
fun removeWhite(pair: Pair<Int, Int>) = GameState(
whiteMen.filter { it != pair },
blackMen,
whiteKings.filter { it != pair },
blackKings,
)
fun removeBlack(pair: Pair<Int, Int>) = GameState(
whiteMen,
blackMen.filter { it != pair },
whiteKings,
blackKings.filter { it != pair },
)
fun addWhiteMan(pair: Pair<Int, Int>) = GameState(
whiteMen + pair,
blackMen,
whiteKings,
blackKings,
)
}
It contains a few functions creating a new state of the game. Please analyze the above code to see how to create an immutable list containing one element less, or one element more, than the previous version.
Future versions of this class will contain code generating and analysing possible movements made by the computer.
The fragment
This is the code of the fragment:
class GameFragment : Fragment(R.layout.fr_game) {
private lateinit var binding: FrGameBinding
private val vm by viewModels<GameVM>()
var isInMovement = false
var pickedFrom = -1 to -1
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FrGameBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.viewModel = vm
return binding.root
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fun predecessor(x1: Int, y1: Int, x2: Int, y2: Int): Pair<Int, Int> {
if (abs(x2 - x1) != abs(y2 - y1) ||
abs(x2 - x1) < 2 || abs(y2 - y1) < 2) return -1 to -1
val dirx = if (x2 - x1 > 0) 1 else -1
val diry = if (y2 - y1 > 0) 1 else -1
return x2 - dirx to y2 - diry
}
binding.pieces.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> if (vm.isWhiteMoving) {
val x = binding.pieces.getX(event.rawX)
val y = binding.pieces.getY(event.rawY)
pickedFrom = x to y
isInMovement = vm.removeWhite(x, y)
}
MotionEvent.ACTION_MOVE -> if (isInMovement) {
vm.moveTo(event.rawX.toInt(), event.rawY.toInt())
}
MotionEvent.ACTION_UP -> if (isInMovement) {
val x = binding.pieces.getX(event.rawX)
val y = binding.pieces.getY(event.rawY)
if (x in 0..7 && y in 0..7 && vm.isEmpty(x, y) &&
abs(x - pickedFrom.first) == 1 &&
(y == pickedFrom.second - 1 ||
y == pickedFrom.second - 2 &&
vm.removeBlack(predecessor(pickedFrom.first, pickedFrom.second, x, y)))) {
vm.addWhite(x, y)
}
else {
vm.addWhite(pickedFrom.first, pickedFrom.second)
}
}
}
true
}
}
}
Please analyze the touch listeners. It performs some validation and then it manipulates the state of the game.
On MotionEvent.ACTION_DOWN
it first calculates the coordinates on the board from the coordinates of the event. This is the code in the View
that performs the conversion:
fun getX(x: Float) = x.toInt() * 8 / measuredWidth
fun getY(y: Float) = y.toInt() * 8 / measuredHeight - 1
The listener then remembers the original position of the man and removes it from the board.
In the data class the man currently in movement is remembered in the field:
val moving: Pair<Int, Int> = -1 to -1
This is the field that is used for animating the movement of the man currently held by the user.
On MotionEvent.ACTION_MOVE
the listener moves (animates) one man.
On `MotionEvent.ACTION_UP’ the listener performs validation of the movement and does some of the following:
- Captures (removes from memory) a black piece
- Stores in memory the new position of the white man
- Puts the white man back in its original position if the movement is invalid
The ViewModel
This is the ViewModel:
class GameVM : ViewModel() {
var isWhiteMoving = true
private var gameState = GameState(WHITE_START_POSITION, BLACK_START_POSITION, emptyList(), emptyList())
private val _gameStateFlow = MutableStateFlow(gameState)
val gameStateFlow: StateFlow<GameState> = _gameStateFlow
fun removeWhite(x: Int, y: Int): Boolean {
val removed = gameState.removeWhite(x to y)
val result = removed != gameState
_gameStateFlow.value = removed
gameState = removed
return result
}
fun addWhite(x: Int, y: Int) {
gameState = gameState.addWhiteMan(x to y)
_gameStateFlow.value = gameState
}
fun moveTo(x: Int, y: Int) {
gameState = gameState.copy(moving = x to y)
_gameStateFlow.value = gameState
}
private fun containsWhite(x: Int, y: Int) = gameState.containsWhite(x to y)
private fun containsBlack(x: Int, y: Int) = gameState.containsBlack(x to y)
fun removeBlack(pair: Pair<Int, Int>): Boolean {
val removed = gameState.removeBlack(pair)
val result = gameState == removed
gameState = removed
return result
}
fun isEmpty(x: Int, y: Int) = !containsWhite(x, y) && !containsBlack(x, y)
companion object {
val WHITE_START_POSITION = listOf(0 to 7, 1 to 6, 2 to 7, 3 to 6, 4 to 7, 5 to 6, 6 to 7, 7 to 6, 0 to 5, 2 to 5, 4 to 5, 6 to 5)
val BLACK_START_POSITION = listOf(0 to 1, 1 to 0, 2 to 1, 3 to 0, 4 to 1, 5 to 0, 6 to 1, 7 to 0, 1 to 2, 3 to 2, 5 to 2, 7 to 2)
}
}
Removing a white piece (man or king) in the function removeWhite()
first creates a version of the game state with one of whie pieces removed. Then it compares the two states to determin whether there has been a change.
If the state is not changed, it means that there was not a white piece in the field in the first place, so movement will not be initiated.
Either way, it sets the new state as a new value of the StateFlow
. Nothing happens if the value is the same. If the value has been changed, the new state is then passed to the view and reflected on the GUI.
Removing a black piece in the function removeBlack()
works in a similar way.
The difference is that in the current version of the game a black piece is removed from memory (captured) only while a white man is moving. There is therefore no need to flush to the screen the intermediate state. The final state is flushed to the screen only when the white man finishes its movement.
Storing a new position of the white man in the function addWhiteMan()
is performed when the white man is finished moving. The new state of the game is then flushed to the screen using the line _gameStateFlow.value = gameState
.
Finishing the movement of the white man means also that the movement is no longer animated. Under the hood, adding one man to the board in its new position creates a new game state by calling the code:
fun addWhiteMan(pair: Pair<Int, Int>) = GameState(
whiteMen + pair,
blackMen,
whiteKings,
blackKings,
)
It calls the constructor of GameState
with the default value (-1 to -1
) of the field moving
:
data class GameState(
val whiteMen: List<Pair<Int, Int>>,
val blackMen: List<Pair<Int, Int>>,
val whiteKings: List<Pair<Int, Int>>,
val blackKings: List<Pair<Int, Int>>,
val moving: Pair<Int, Int> = -1 to -1,
)
Negative values mean that no piece is currently moving.
Conclusion
The article explained how to move checkers pieces in two dimension using a handful of immutable lists.
Creating new versions of the lists have been demonstrated using filtering and concatenation.
Animation of the piece currently being moved has been briefly discussed.
Ground has been prepared for implementation of computer’s movement by creating a tree of possible moves represented by immutable game states.