I'm using images (.png) to show the "X" and "O" in the game. But it appears that they don't show up until the game is over.
The game logic:
enum class Player {
X, O
}
enum class GameState {
ONGOING, DRAW, X_WON, O_WON
}
class TicTacToe {
var board by mutableStateOf(Array(3) { CharArray(3) { ' ' } })
var currentPlayer by mutableStateOf(Player.X)
var gameState by mutableStateOf(GameState.ONGOING)
fun makeMove(
row: Int,
col: Int,
board: Array<CharArray>,
) {
if(board[row][col] != ' ') return
board[row][col] = currentPlayer.name.first()
currentPlayer = if(currentPlayer == Player.X) {
Player.O
} else {
Player.X
}
gameState = getGameState(board)
}
fun getGameState(
board: Array<CharArray>,
): GameState {
var winner = ' '
//rows and columns
for (i in 0..2) {
if (board[i][0] != ' ' && board[i][0] == board[i][1] && board[i][1] == board[i][2]) {
winner = board[i][0]
break
}
if (board[0][i] != ' ' && board[0][i] == board[1][i] && board[1][i] == board[2][i]) {
winner = board[0][i]
break
}
}
// Diagonals
if (winner == ' ' && board[0][0] != ' ' && board[0][0] == board[1][1] && board[1][1] == board[2][2]) {
winner = board[0][0]
}
if (winner == ' ' && board[0][2] != ' ' && board[0][2] == board[1][1] && board[1][1] == board[2][0]) {
winner = board[0][2]
}
return when (winner) {
'X' -> GameState.X_WON
'O' -> GameState.O_WON
else -> {
val isDraw = board.all { row -> row.all { cell -> cell != ' ' } }
if (isDraw) GameState.DRAW else GameState.ONGOING
}
}
}
fun reset(
board: Array<CharArray>,
) {
for (i in 0..2) {
for (j in 0..2) {
board[i][j] = ' '
}
}
gameState = GameState.ONGOING
}
}
The game screen:
@Composable
fun GameScreen(
modifier: Modifier = Modifier,
) {
val recomposeTrigger = remember { mutableIntStateOf(0) }
val game = remember { mutableStateOf(TicTacToe()) }
if(game.value.gameState == GameState.DRAW || game.value.gameState == GameState.O_WON || game.value.gameState == GameState.X_WON) {
GameEndModal(
gameState = game.value.gameState,
onPlayAgain = {
game.value.reset(game.value.board)
}
)
}
Box(
modifier = modifier
.size(300.dp)
.background(Color(0xFFBA00FF))
.padding(16.dp),
contentAlignment = Alignment.Center
) {
TicTacToeGrid()
Column(
modifier = Modifier.size(300.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
for(row in 0..2) {
Row(
modifier = Modifier
.weight(1f)
) {
for(col in 0..2) {
val imageRes: Painter? = when(game.value.board[row][col]) {
'X' -> {
painterResource(R.drawable.x)
}
'O' -> {
painterResource(R.drawable.o)
}
else -> {
null
}
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxSize()
.clickable {
game.value.makeMove(row, col, board = game.value.board)
recomposeTrigger.value++
},
contentAlignment = Alignment.Center
) {
if (imageRes != null) {
Image(
painter = imageRes,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.padding(4.dp)
)
}
}
}
}
}
}
}
}
Image mid-play: Clicking the boxes is not recomposing the GameScreen composable
Image end: At the end the images are loaded properly
I made a clickable box for the user to interact with.
And I even tried to force recompose the composable by defining a variable and incrementing it at the box click (which I know is not a good practice), but still it didn't work.
The problem is that, although the board
is backed by a MutableState, the content itself is mutable. This assignment does not change the MutableState:
board[row][col] = currentPlayer.name.first()
Instead, it only modifies the array inside the MutableState, which does not trigger a recomposition of your UI.
A simple solution would be to only work on a copy of the array and, once done, assign this new array to board
:
fun makeMove(
row: Int,
col: Int,
) {
val newBoard = board.copyOf()
if(newBoard[row][col] != ' ') return
newBoard[row][col] = currentPlayer.name.first()
currentPlayer = if(currentPlayer == Player.X) {
Player.O
} else {
Player.X
}
board = newBoard
gameState = getGameState(newBoard)
}
Please not that I also removed the board
parameter as that seems to be always the same as the already existing board
property of the same name.
Just omit the last parameter when called:
game.value.makeMove(row, col)